#!/usr/bin/env python3
"""
Build artifact analysis module for AURIX projects.

Parsers for:
  - TASKING VX-toolset linker MAP files
  - GCC tricore-elf-objdump listing (.lst) files
  - TASKING Module Definition Files (.mdf)
  - ELF files (both TASKING and GCC, via pyelftools)
  - Build execution and output parsing
"""

from __future__ import annotations

import bisect
import os
import re
import shlex
import subprocess
import sys
from pathlib import Path
from typing import Optional


# ---------------------------------------------------------------------------
# TASKING MAP Parser
# ---------------------------------------------------------------------------

class TaskingMapParser:
    """Parse TASKING VX-toolset linker MAP files.

    TASKING MAP files have structured sections separated by header lines:
      * Memory usage in bytes
      * Space usage in bytes
      * Sections
      * Symbols (sorted on name)
      * Symbols (sorted on address)
      * Unreferenced sections
    """

    def __init__(self):
        self._memory_usage: list[dict] = []
        self._sections: list[dict] = []
        self._symbols_by_name: list[dict] = []
        self._symbols_by_addr: list[dict] = []
        self._unreferenced: list[str] = []
        self._tool_info: dict = {}
        self._parsed = False

    def parse(self, filepath: str) -> dict:
        """Parse a TASKING MAP file and return structured data."""
        with open(filepath, "r", encoding="utf-8", errors="replace") as f:
            lines = f.readlines()

        self._parse_tool_info(lines)
        self._parse_memory_usage(lines)
        self._parse_sections(lines)
        self._parse_symbols_by_name(lines)
        self._parse_symbols_by_addr(lines)
        self._parse_unreferenced(lines)
        self._parsed = True

        return {
            "tool_info": self._tool_info,
            "memory_usage": self._memory_usage,
            "sections": self._sections,
            "symbols_by_name": self._symbols_by_name,
            "symbols_by_addr": self._symbols_by_addr,
            "unreferenced_count": len(self._unreferenced),
        }

    # -- Internal parsers --------------------------------------------------

    def _find_section_start(self, lines: list[str], marker: str) -> int:
        """Find the line index containing a section marker."""
        for i, line in enumerate(lines):
            if marker in line:
                return i
        return -1

    def _parse_tool_info(self, lines: list[str]):
        """Extract tool name, path, and arguments from the header."""
        for line in lines[:60]:
            if "| tool" in line:
                parts = [p.strip() for p in line.split("|") if p.strip()]
                if len(parts) >= 2:
                    self._tool_info["tool"] = parts[1]
            elif "| path" in line:
                parts = [p.strip() for p in line.split("|") if p.strip()]
                if len(parts) >= 2:
                    self._tool_info["path"] = parts[1]

    def _parse_memory_usage(self, lines: list[str]):
        """Parse the 'Memory usage in bytes' table.

        Format: | mpe:dsram0 | 0x0 | 0x003362 | 0x006900 | 0x02639e | 0x030000 |
        """
        start = self._find_section_start(lines, "* Memory usage in bytes")
        if start < 0:
            return
        # Pattern: | <name> | <code> | <data> | <reserved> | <free> | <total> |
        pat = re.compile(
            r"\|\s*(mpe:\S+)\s*\|\s*(0x[0-9a-fA-F]+)\s*\|\s*(0x[0-9a-fA-F]+)\s*\|"
            r"\s*(0x[0-9a-fA-F]+)\s*\|\s*(0x[0-9a-fA-F]+)\s*\|\s*(0x[0-9a-fA-F]+)\s*\|"
        )
        for line in lines[start:start + 30]:
            m = pat.match(line)
            if m:
                self._memory_usage.append({
                    "name": m.group(1),
                    "code": int(m.group(2), 16),
                    "data": int(m.group(3), 16),
                    "reserved": int(m.group(4), 16),
                    "free": int(m.group(5), 16),
                    "total": int(m.group(6), 16),
                })

    def _parse_sections(self, lines: list[str]):
        """Parse the 'Sections' table.

        Format: | mpe:dsram0 | data | .bss.Mcu_Core.Os_Csas_Core0 (232) | 0x002000 | 0x70000940 | ... |
        Handles multi-line rows (section name wraps to next line).
        """
        start = self._find_section_start(lines, "* Sections")
        if start < 0:
            return
        # Pattern for a complete section row
        pat = re.compile(
            r"\|\s*(mpe:\S+)\s*\|\s*(\S+)\s*\|\s*(.+?)\s*\|"
            r"\s*(0x[0-9a-fA-F]+)\s*\|\s*(0x[0-9a-fA-F]+)\s*\|\s*(0x[0-9a-fA-F]+)\s*\|\s*(0x[0-9a-fA-F]+)\s*\|"
        )
        for i, line in enumerate(lines[start:]):
            # Stop at next major section (skip the marker line itself)
            if i > 0 and line.startswith("* "):
                break
            m = pat.match(line)
            if m:
                sec_name = m.group(3).strip()
                self._sections.append({
                    "chip": m.group(1),
                    "group": m.group(2),
                    "section": sec_name,
                    "size": int(m.group(4), 16),
                    "space_addr": int(m.group(5), 16),
                    "chip_addr": int(m.group(6), 16),
                    "alignment": int(m.group(7), 16),
                })

    def _parse_symbols_by_name(self, lines: list[str]):
        """Parse '* Symbols (sorted on name)' section.

        Format: | Name | Space addr | Space |
        """
        start = self._find_section_start(lines, "* Symbols (sorted on name)")
        if start < 0:
            return
        pat = re.compile(
            r"\|\s*(\S+)\s*\|\s*(0x[0-9a-fA-F]+)\s*\|\s*(\S*)\s*\|"
        )
        last_space = ""
        for line in lines[start:]:
            if line.startswith("* ") and "sorted on address" in line:
                break
            m = pat.match(line)
            if m:
                space = m.group(3) or last_space
                if space:
                    last_space = space
                self._symbols_by_name.append({
                    "name": m.group(1),
                    "address": int(m.group(2), 16),
                    "space": space,
                })

    def _parse_symbols_by_addr(self, lines: list[str]):
        """Parse '* Symbols (sorted on address)' section.

        Format: | Space addr | Name | Space |
        """
        start = self._find_section_start(lines, "* Symbols (sorted on address)")
        if start < 0:
            return
        pat = re.compile(
            r"\|\s*(0x[0-9a-fA-F]+)\s*\|\s*(\S+)\s*\|\s*(\S*)\s*\|"
        )
        last_space = ""
        for line in lines[start:]:
            if line.startswith("* ") and "Unreferenced" in line:
                break
            m = pat.match(line)
            if m:
                space = m.group(3) or last_space
                if space:
                    last_space = space
                self._symbols_by_addr.append({
                    "name": m.group(2),
                    "address": int(m.group(1), 16),
                    "space": space,
                })

    def _parse_unreferenced(self, lines: list[str]):
        """Parse '* Unreferenced' section at end of MAP file."""
        start = self._find_section_start(lines, "Unreferenced")
        if start < 0:
            return
        pat = re.compile(r"\|\s*(.+?)\s*\|.*\|\s*unreferenced\s*\|")
        for line in lines[start:]:
            m = pat.match(line)
            if m:
                self._unreferenced.append(m.group(1).strip())

    # -- Query API ---------------------------------------------------------

    def get_memory_summary(self) -> dict:
        """Return memory usage summary."""
        if not self._parsed:
            return {"error": "File not parsed yet"}
        total_code = sum(r["code"] for r in self._memory_usage)
        total_data = sum(r["data"] for r in self._memory_usage)
        return {
            "regions": self._memory_usage,
            "total_code": total_code,
            "total_data": total_data,
            "region_count": len(self._memory_usage),
        }

    def get_sections(self, chip_filter: str = "") -> list[dict]:
        """Return section layout, optionally filtered by chip."""
        if not self._parsed:
            return []
        if chip_filter:
            return [s for s in self._sections if chip_filter in s["chip"]]
        return self._sections

    def lookup_symbol(self, name: str) -> Optional[dict]:
        """Look up symbol by exact name."""
        if not self._parsed:
            return None
        for sym in self._symbols_by_name:
            if sym["name"] == name:
                return {"name": sym["name"], "address": sym["address"],
                        "space": sym["space"], "source": "tasking_map"}
        return None

    def lookup_address(self, addr: int) -> Optional[dict]:
        """Find nearest symbol for a given address."""
        if not self._parsed or not self._symbols_by_addr:
            return None
        # Binary search by address
        addrs = [s["address"] for s in self._symbols_by_addr]
        idx = bisect.bisect_right(addrs, addr) - 1
        if idx < 0:
            return None
        sym = self._symbols_by_addr[idx]
        return {
            "name": sym["name"],
            "address": sym["address"],
            "offset": addr - sym["address"],
            "space": sym["space"],
            "source": "tasking_map",
        }

    def get_all_symbols(self, sort_by: str = "name",
                        search: str = "", max_results: int = 200) -> list[dict]:
        """Return symbols with optional filter."""
        if not self._parsed:
            return []
        src = self._symbols_by_name if sort_by == "name" else self._symbols_by_addr
        if search:
            src = [s for s in src if search.lower() in s["name"].lower()]
        return src[:max_results]


# ---------------------------------------------------------------------------
# GCC LST Parser
# ---------------------------------------------------------------------------

class GccLstParser:
    """Parse GCC tricore-elf-objdump listing (.lst) files.

    The .lst file is generated by: tricore-elf-objdump -h -S <file>.elf
    Contains: section headers + disassembly with C source interleaved.
    """

    # Pattern for section header rows:
    #   0 .CPU0.ustack  00000800  7002aa00  7002aa00  00000274  2**0
    _SECTION_PAT = re.compile(
        r"^\s*(\d+)\s+(\S+)\s+([0-9a-fA-F]+)\s+([0-9a-fA-F]+)\s+([0-9a-fA-F]+)\s+([0-9a-fA-F]+)\s+2\*\*(\d+)"
    )
    # Pattern for symbol labels in disassembly:
    #   80000100 <IfxCpu_Trap_vectorTable0>:
    _SYMBOL_PAT = re.compile(r"^([0-9a-fA-F]{8})\s+<(.+)>:$")
    # Pattern for flags line:
    _FLAGS_PAT = re.compile(r"^\s+((?:CONTENTS|ALLOC|LOAD|READONLY|CODE|DATA|SMALL_DATA|DEBUGGING)[\s,]*)")

    def __init__(self):
        self._sections: list[dict] = []
        self._symbols: list[dict] = []
        self._parsed = False

    def parse(self, filepath: str) -> dict:
        """Parse a GCC .lst file."""
        with open(filepath, "r", encoding="utf-8", errors="replace") as f:
            lines = f.readlines()

        self._parse_sections(lines)
        self._parse_symbols(lines)
        self._parsed = True

        return {
            "sections": self._sections,
            "symbol_count": len(self._symbols),
        }

    def _parse_sections(self, lines: list[str]):
        """Parse section headers from the beginning of the .lst file."""
        pending_flags = None
        for line in lines:
            m = self._SECTION_PAT.match(line)
            if m:
                # If there was a pending section, attach flags
                if pending_flags and self._sections:
                    self._sections[-1]["flags"] = pending_flags
                pending_flags = ""
                self._sections.append({
                    "idx": int(m.group(1)),
                    "name": m.group(2),
                    "size": int(m.group(3), 16),
                    "vma": int(m.group(4), 16),
                    "lma": int(m.group(5), 16),
                    "file_off": int(m.group(6), 16),
                    "align": 2 ** int(m.group(7)),
                    "flags": "",
                })
            else:
                fm = self._FLAGS_PAT.match(line)
                if fm and self._sections:
                    flags = fm.group(1).strip()
                    self._sections[-1]["flags"] = flags
                    pending_flags = None

    def _parse_symbols(self, lines: list[str]):
        """Extract symbol labels from disassembly."""
        for line in lines:
            m = self._SYMBOL_PAT.match(line.strip())
            if m:
                self._symbols.append({
                    "name": m.group(2),
                    "address": int(m.group(1), 16),
                })
        # Sort by address for binary search
        self._symbols.sort(key=lambda s: s["address"])

    def get_sections(self) -> list[dict]:
        if not self._parsed:
            return []
        return self._sections

    def get_symbols(self) -> list[dict]:
        if not self._parsed:
            return []
        return self._symbols

    def lookup_symbol(self, name: str) -> Optional[dict]:
        if not self._parsed:
            return None
        for sym in self._symbols:
            if sym["name"] == name:
                return {**sym, "source": "gcc_lst"}
        return None

    def lookup_address(self, addr: int) -> Optional[dict]:
        if not self._parsed or not self._symbols:
            return None
        addrs = [s["address"] for s in self._symbols]
        idx = bisect.bisect_right(addrs, addr) - 1
        if idx < 0:
            return None
        sym = self._symbols[idx]
        return {
            "name": sym["name"],
            "address": sym["address"],
            "offset": addr - sym["address"],
            "source": "gcc_lst",
        }

    def get_all_symbols(self, search: str = "",
                        max_results: int = 200) -> list[dict]:
        if not self._parsed:
            return []
        result = self._symbols
        if search:
            result = [s for s in result if search.lower() in s["name"].lower()]
        return result[:max_results]


# ---------------------------------------------------------------------------
# TASKING MDF Parser
# ---------------------------------------------------------------------------

class MdfParser:
    """Parse TASKING Module Definition Files (.mdf).

    Format (tab-separated):
      space <name> <id> <mau_size>
      chip  <name> <id> <mau_size> <size> <type>
      map   <space_id> <start_in_space> <size_in_space> <chip_id> <start_on_chip>
    """

    def __init__(self):
        self._spaces: list[dict] = []
        self._chips: list[dict] = []
        self._mappings: list[dict] = []
        self._parsed = False

    def parse(self, filepath: str) -> dict:
        """Parse an MDF file."""
        with open(filepath, "r", encoding="utf-8", errors="replace") as f:
            for line in f:
                line = line.strip()
                if not line or line.startswith("#"):
                    continue
                parts = line.split("\t")
                if parts[0] == "space" and len(parts) >= 4:
                    self._spaces.append({
                        "name": parts[1],
                        "id": int(parts[2]),
                        "mau_size": int(parts[3]),
                    })
                elif parts[0] == "chip" and len(parts) >= 6:
                    self._chips.append({
                        "name": parts[1],
                        "id": int(parts[2]),
                        "mau_size": int(parts[3]),
                        "size": int(parts[4]),
                        "type": parts[5],
                    })
                elif parts[0] == "map" and len(parts) >= 6:
                    self._mappings.append({
                        "space_id": int(parts[1]),
                        "start_in_space": int(parts[2]),
                        "size_in_space": int(parts[3]),
                        "chip_id": int(parts[4]),
                        "start_on_chip": int(parts[5]),
                    })
        self._parsed = True
        return {
            "spaces": self._spaces,
            "chips": self._chips,
            "mappings": self._mappings,
        }

    def get_memory_regions(self) -> list[dict]:
        """Return chip-level memory regions."""
        if not self._parsed:
            return []
        regions = []
        for chip in self._chips:
            # Find first mapping for this chip to get start address
            start_addr = 0
            for m in self._mappings:
                if m["chip_id"] == chip["id"]:
                    start_addr = m["start_in_space"]
                    break
            regions.append({
                "name": chip["name"],
                "type": chip["type"],
                "size": chip["size"],
                "start_addr": start_addr,
            })
        return regions

    def translate_address(self, space_addr: int) -> Optional[dict]:
        """Translate a space address to chip physical address."""
        if not self._parsed:
            return None
        for m in self._mappings:
            if m["start_in_space"] <= space_addr < m["start_in_space"] + m["size_in_space"]:
                offset = space_addr - m["start_in_space"]
                chip = next((c for c in self._chips if c["id"] == m["chip_id"]), None)
                chip_name = chip["name"] if chip else f"chip_{m['chip_id']}"
                space = next((s for s in self._spaces if s["id"] == m["space_id"]), None)
                space_name = space["name"] if space else f"space_{m['space_id']}"
                return {
                    "chip": chip_name,
                    "chip_addr": m["start_on_chip"] + offset,
                    "space": space_name,
                    "space_addr": space_addr,
                    "offset_in_mapping": offset,
                }
        return None


# ---------------------------------------------------------------------------
# ELF Parser (pyelftools)
# ---------------------------------------------------------------------------

class ElfParser:
    """Parse ELF files using pyelftools.

    Compatible with both TASKING and GCC TriCore ELF files.
    """

    def __init__(self):
        self._symbols: list[dict] = []
        self._sections: list[dict] = []
        self._functions: list[dict] = []
        self._variables: list[dict] = []
        self._dwarf_available = False
        self._filepath: str = ""
        self._parsed = False

    def parse(self, filepath: str) -> dict:
        """Parse an ELF file."""
        try:
            from elftools.elf.elffile import ELFFile
            from elftools.elf.sections import SymbolTableSection
        except ImportError:
            return {"error": "pyelftools not installed. Run: pip install pyelftools"}

        self._filepath = filepath
        with open(filepath, "rb") as f:
            elf = ELFFile(f)

            # Sections
            for sec in elf.iter_sections():
                self._sections.append({
                    "name": sec.name,
                    "address": sec["sh_addr"],
                    "size": sec["sh_size"],
                    "type": sec["sh_type"],
                    "flags": sec["sh_flags"],
                })

            # Symbols from .symtab and .dynsym
            for sec in elf.iter_sections():
                if isinstance(sec, SymbolTableSection):
                    for sym in sec.iter_symbols():
                        name = sym.name
                        if not name:
                            continue
                        entry = {
                            "name": name,
                            "address": sym["st_value"],
                            "size": sym["st_size"],
                            "type": sym["st_info"]["type"],
                            "bind": sym["st_info"]["bind"],
                            "section_idx": sym["st_shndx"],
                        }
                        self._symbols.append(entry)
                        if entry["type"] == "STT_FUNC":
                            self._functions.append(entry)
                        elif entry["type"] == "STT_OBJECT":
                            self._variables.append(entry)

            # Check for DWARF debug info
            self._dwarf_available = elf.has_dwarf_info()

            # Get ELF class from the file header (32 or 64 bit)
            elf_class = f"ELF{elf.elfclass}" if hasattr(elf, 'elfclass') else "unknown"

        # Sort symbols by address for binary search
        self._symbols.sort(key=lambda s: s["address"])
        self._parsed = True

        return {
            "symbols": len(self._symbols),
            "functions": len(self._functions),
            "variables": len(self._variables),
            "sections": len(self._sections),
            "dwarf_available": self._dwarf_available,
            "elf_class": elf_class,
        }

    def get_symbols(self) -> list[dict]:
        if not self._parsed:
            return []
        return self._symbols

    def get_sections(self) -> list[dict]:
        if not self._parsed:
            return []
        return self._sections

    def get_functions(self) -> list[dict]:
        if not self._parsed:
            return []
        return self._functions

    def get_variables(self) -> list[dict]:
        if not self._parsed:
            return []
        return self._variables

    def get_source_location(self, addr: int) -> Optional[dict]:
        """Look up source file/line for an address (requires DWARF)."""
        if not self._parsed or not self._dwarf_available:
            return None
        try:
            from elftools.elf.elffile import ELFFile
            with open(self._filepath, "rb") as f:
                elf = ELFFile(f)
                dwarf = elf.get_dwarf_info()
                for CU in dwarf.iter_CUs():
                    lp = dwarf.line_program_for_CU(CU)
                    if lp is None:
                        continue
                    prev_state = None
                    for entry in lp.get_entries():
                        state = entry.state
                        if state is None:
                            continue
                        if prev_state and prev_state.address <= addr < state.address:
                            file_entry = lp["file_entry"][prev_state.file - 1]
                            dir_entry = lp["include_directory"][file_entry["dir_index"] - 1] if file_entry["dir_index"] > 0 else b""
                            return {
                                "file": os.path.join(
                                    dir_entry.decode("utf-8", errors="replace") if isinstance(dir_entry, bytes) else str(dir_entry),
                                    file_entry.name.decode("utf-8", errors="replace") if isinstance(file_entry.name, bytes) else str(file_entry.name),
                                ),
                                "line": prev_state.line,
                                "address": prev_state.address,
                            }
                        prev_state = state
        except Exception:
            pass
        return None

    def lookup_symbol(self, name: str) -> Optional[dict]:
        """Look up symbol by exact name."""
        if not self._parsed:
            return None
        for sym in self._symbols:
            if sym["name"] == name:
                return {**sym, "source": "elf"}
        return None

    def lookup_address(self, addr: int) -> Optional[dict]:
        """Find nearest symbol for a given address."""
        if not self._parsed or not self._symbols:
            return None
        addrs = [s["address"] for s in self._symbols]
        idx = bisect.bisect_right(addrs, addr) - 1
        if idx < 0:
            return None
        sym = self._symbols[idx]
        result = {
            "name": sym["name"],
            "address": sym["address"],
            "offset": addr - sym["address"],
            "type": sym["type"],
            "source": "elf",
        }
        # Try DWARF source lookup for code addresses
        src_loc = self.get_source_location(addr)
        if src_loc:
            result["source_location"] = src_loc
        return result

    def get_all_symbols(self, search: str = "", sym_type: str = "",
                        max_results: int = 200) -> list[dict]:
        """Return filtered symbols."""
        if not self._parsed:
            return []
        result = self._symbols
        if search:
            result = [s for s in result if search.lower() in s["name"].lower()]
        if sym_type:
            result = [s for s in result if s["type"] == sym_type]
        return result[:max_results]


# ---------------------------------------------------------------------------
# Build Runner
# ---------------------------------------------------------------------------

class BuildRunner:
    """Execute user build commands and parse output."""

    # Common error patterns
    _TASKING_ERROR_PAT = re.compile(r"(.+?)\((\d+)\):\s*error\s+(E\d+):\s*(.*)")
    _TASKING_WARN_PAT = re.compile(r"(.+?)\((\d+)\):\s*warning\s+(W\d+):\s*(.*)")
    _GCC_ERROR_PAT = re.compile(r"(.+?):(\d+):(\d+):\s*error:\s*(.*)")
    _GCC_WARN_PAT = re.compile(r"(.+?):(\d+):(\d+):\s*warning:\s*(.*)")

    def run(self, command: str, work_dir: Optional[str] = None,
            timeout: int = 300) -> dict:
        """Execute a build command and return structured result."""
        if not command:
            return {"error": "No build command specified"}

        cwd = work_dir or os.getcwd()

        try:
            # Safe command parsing — no shell=True to prevent command injection
            if sys.platform == "win32":
                # Windows: .bat/.cmd files need cmd.exe wrapper
                first_token = command.split()[0] if command.split() else ""
                if first_token.lower().endswith((".bat", ".cmd")):
                    cmd_args = ["cmd.exe", "/c"] + shlex.split(command, posix=False)
                else:
                    cmd_args = shlex.split(command, posix=False)
            else:
                cmd_args = shlex.split(command)
            result = subprocess.run(
                cmd_args,
                shell=False,
                capture_output=True,
                text=True,
                timeout=timeout,
                cwd=cwd,
            )
        except subprocess.TimeoutExpired:
            return {"error": f"Build timeout after {timeout}s", "success": False}
        except Exception as e:
            return {"error": str(e), "success": False}

        stdout = result.stdout
        stderr = result.stderr
        errors, warnings = self._parse_build_output(stdout, stderr)

        # Detect output files in work_dir
        output_files = self.detect_outputs(cwd)

        return {
            "success": result.returncode == 0,
            "returncode": result.returncode,
            "errors": errors,
            "warnings": warnings,
            "error_count": len(errors),
            "warning_count": len(warnings),
            "output_files": output_files,
            "stdout_tail": "\n".join(stdout.splitlines()[-20:]) if stdout else "",
        }

    def detect_outputs(self, work_dir: str) -> dict:
        """Detect build output files in the working directory."""
        outputs = {}
        wp = Path(work_dir)
        for ext, key in [(".elf", "elf_file"), (".map", "map_file"),
                         (".lst", "lst_file"), (".hex", "hex_file"),
                         (".mdf", "mdf_file")]:
            matches = list(wp.glob(f"*{ext}"))
            if matches:
                outputs[key] = str(matches[0])
        return outputs

    def _parse_build_output(self, stdout: str, stderr: str) -> tuple[list[dict], list[dict]]:
        """Extract errors and warnings from compiler output."""
        errors = []
        warnings = []
        combined = stdout + "\n" + stderr

        for line in combined.splitlines():
            # TASKING format
            m = self._TASKING_ERROR_PAT.match(line)
            if m:
                errors.append({"file": m.group(1), "line": int(m.group(2)),
                               "code": m.group(3), "message": m.group(4)})
                continue
            m = self._TASKING_WARN_PAT.match(line)
            if m:
                warnings.append({"file": m.group(1), "line": int(m.group(2)),
                                 "code": m.group(3), "message": m.group(4)})
                continue
            # GCC format
            m = self._GCC_ERROR_PAT.match(line)
            if m:
                errors.append({"file": m.group(1), "line": int(m.group(2)),
                               "column": int(m.group(3)), "message": m.group(4)})
                continue
            m = self._GCC_WARN_PAT.match(line)
            if m:
                warnings.append({"file": m.group(1), "line": int(m.group(2)),
                                 "column": int(m.group(3)), "message": m.group(4)})

        return errors, warnings
