#!/usr/bin/env python3
import argparse
import os
import re
import sys
from logging import INFO, WARNING, Filter, Formatter, StreamHandler, getLogger
from xml.etree import ElementTree as ET


class SingleLevelFilter(Filter):
    def __init__(self, passlevel, reject):
        self.passlevel = passlevel
        self.reject = reject

    def filter(self, record):
        if self.reject:
            return record.levelno != self.passlevel
        return record.levelno == self.passlevel


class Actor:
    def __init__(self, mod_name, vfs_path):
        self.mod_name = mod_name
        self.vfs_path = vfs_path
        self.name = os.path.basename(vfs_path)
        self.textures = []
        self.material = ""
        self.logger = getLogger(__name__)

    def read(self, physical_path):
        try:
            tree = ET.parse(physical_path)
        except ET.ParseError:
            self.logger.exception(physical_path)
            return False
        root = tree.getroot()
        # Special case: particles don't need a diffuse texture.
        if len(root.findall(".//particles")) > 0:
            self.textures.append("baseTex")

        for element in root.findall(".//material"):
            self.material = element.text
        for element in root.findall(".//texture"):
            self.textures.append(element.get("name"))
        for element in root.findall(".//variant"):
            file = element.get("file")
            if file:
                self.read_variant(physical_path, os.path.join("art", "variants", file))
        return True

    def read_variant(self, actor_physical_path, relative_path):
        physical_path = actor_physical_path.replace(self.vfs_path, relative_path)
        try:
            tree = ET.parse(physical_path)
        except ET.ParseError:
            self.logger.exception(physical_path)
            return

        root = tree.getroot()
        file = root.get("file")
        if file:
            self.read_variant(actor_physical_path, os.path.join("art", "variants", file))

        for element in root.findall(".//texture"):
            self.textures.append(element.get("name"))


class Material:
    def __init__(self, mod_name, vfs_path):
        self.mod_name = mod_name
        self.vfs_path = vfs_path
        self.name = os.path.basename(vfs_path)
        self.required_textures = []

    def read(self, physical_path):
        try:
            root = ET.parse(physical_path).getroot()
        except ET.ParseError:
            self.logger.exception(physical_path)
            return False
        for element in root.findall(".//required_texture"):
            texture_name = element.get("name")
            self.required_textures.append(texture_name)
        return True


class Validator:
    def __init__(self, vfs_root, mods=None):
        if mods is None:
            mods = ["mod", "public"]

        self.vfs_root = vfs_root
        self.mods = mods
        self.materials = {}
        self.invalid_materials = {}
        self.actors = []
        self.__init_logger()

    def __init_logger(self):
        logger = getLogger(__name__)
        logger.setLevel(INFO)
        # create a console handler, seems nicer to Windows and for future uses
        ch = StreamHandler(sys.stdout)
        ch.setLevel(INFO)
        ch.setFormatter(Formatter("%(levelname)s - %(message)s"))
        f1 = SingleLevelFilter(INFO, False)
        ch.addFilter(f1)
        logger.addHandler(ch)
        errorch = StreamHandler(sys.stderr)
        errorch.setLevel(WARNING)
        errorch.setFormatter(Formatter("%(levelname)s - %(message)s"))
        logger.addHandler(errorch)
        self.logger = logger
        self.inError = False

    def get_mod_path(self, mod_name, vfs_path):
        return os.path.join(mod_name, vfs_path)

    def get_physical_path(self, mod_name, vfs_path):
        return os.path.realpath(os.path.join(self.vfs_root, mod_name, vfs_path))

    def find_mod_files(self, mod_name, vfs_path, pattern):
        physical_path = self.get_physical_path(mod_name, vfs_path)
        result = []
        if not os.path.isdir(physical_path):
            return result
        for file_name in os.listdir(physical_path):
            if file_name in (".git", ".svn"):
                continue
            vfs_file_path = os.path.join(vfs_path, file_name)
            physical_file_path = os.path.join(physical_path, file_name)
            if os.path.isdir(physical_file_path):
                result += self.find_mod_files(mod_name, vfs_file_path, pattern)
            elif os.path.isfile(physical_file_path) and pattern.match(file_name):
                result.append({"mod_name": mod_name, "vfs_path": vfs_file_path})
        return result

    def find_all_mods_files(self, vfs_path, pattern):
        result = []
        for mod_name in reversed(self.mods):
            result += self.find_mod_files(mod_name, vfs_path, pattern)
        return result

    def find_materials(self, vfs_path):
        self.logger.info("Collecting materials...")
        material_files = self.find_all_mods_files(vfs_path, re.compile(r".*\.xml"))
        for material_file in material_files:
            material_name = os.path.basename(material_file["vfs_path"])
            if material_name in self.materials:
                continue
            material = Material(material_file["mod_name"], material_file["vfs_path"])
            if material.read(
                self.get_physical_path(material_file["mod_name"], material_file["vfs_path"])
            ):
                self.materials[material_name] = material
            else:
                self.invalid_materials[material_name] = material

    def find_actors(self, vfs_path):
        self.logger.info("Collecting actors...")

        actor_files = self.find_all_mods_files(vfs_path, re.compile(r".*\.xml"))
        for actor_file in actor_files:
            actor = Actor(actor_file["mod_name"], actor_file["vfs_path"])
            if actor.read(self.get_physical_path(actor_file["mod_name"], actor_file["vfs_path"])):
                self.actors.append(actor)

    def run(self):
        self.find_materials(os.path.join("art", "materials"))
        self.find_actors(os.path.join("art", "actors"))
        self.logger.info("Validating textures...")

        for actor in self.actors:
            if not actor.material:
                continue
            if (
                actor.material not in self.materials
                and actor.material not in self.invalid_materials
            ):
                self.logger.error(
                    '"%s": unknown material "%s"',
                    self.get_mod_path(actor.mod_name, actor.vfs_path),
                    actor.material,
                )
                self.inError = True
            if actor.material not in self.materials:
                continue
            material = self.materials[actor.material]

            missing_textures = ", ".join(
                {
                    required_texture
                    for required_texture in material.required_textures
                    if required_texture not in actor.textures
                }
            )
            if len(missing_textures) > 0:
                self.logger.error(
                    '"%s": actor does not contain required texture(s) "%s" from "%s"',
                    self.get_mod_path(actor.mod_name, actor.vfs_path),
                    missing_textures,
                    material.name,
                )
                self.inError = True

            extra_textures = ", ".join(
                {
                    extra_texture
                    for extra_texture in actor.textures
                    if extra_texture not in material.required_textures
                }
            )
            if len(extra_textures) > 0:
                self.logger.warning(
                    '"%s": actor contains unnecessary texture(s) "%s" from "%s"',
                    self.get_mod_path(actor.mod_name, actor.vfs_path),
                    extra_textures,
                    material.name,
                )
                self.inError = True

        return not self.inError


if __name__ == "__main__":
    script_dir = os.path.dirname(os.path.realpath(__file__))
    default_root = os.path.join(script_dir, "..", "..", "..", "binaries", "data", "mods")
    parser = argparse.ArgumentParser(description="Actors/materials validator.")
    parser.add_argument("-r", "--root", action="store", dest="root", default=default_root)
    parser.add_argument("-m", "--mods", action="store", dest="mods", default="mod,public")
    args = parser.parse_args()
    validator = Validator(args.root, args.mods.split(","))
    if not validator.run():
        sys.exit(1)
