import { structureToString, parseHTMLStructure, HTMLTag, HTMLText, ElementStructure } from "./htmlStructure"
import * as Helper from "./helper"
import * as CoreLogic from "./coreLogic"
import * as Leaf from "../../build/leaf.export"
import * as fs from "fs"
import * as pathModule from "path"
import * as ts from "typescript"
//https://github.com/vvakame/typescript-formatter/blob/master/lib/formatter.ts
import * as formatTypescript from "typescript-formatter/lib/formatter"

// Context for template
type WidgetTree = {
    [index: string]: WidgetTree | WidgetTemplate
}
interface DebugContext {
    file: string
    path: string
    name: string
}
export class Context {
    public widgets: WidgetTemplate[] = []
    constructor(public readonly path: string, public debug: boolean = false) {
    }
    public rootName: string = "R"
    includeWidgetByRelativeFilePath(path: string) {
        let content = fs.readFileSync(pathModule.join(this.path, path), "utf8")
        let tags = parseHTMLStructure(content)
        let ext = pathModule.extname(path)
        let base = pathModule.basename(path, ext)
        let dir = pathModule.dirname(path)
        let fullBase = pathModule.join(dir, base)
        if (fullBase[0] != "/") fullBase = "/" + fullBase
        this.widgets.push(new WidgetTemplate(this, tags, fullBase))
    }
    compile(): string {
        // 1. build widgets into tree
        // 2. recursively generate widget from tree
        let tree = this.buildTree(this.widgets)
        let result = this.recursiveGenerate(tree)
        let widgetPathes = this.getAllWidgetPath(this.widgets)
        let extra = `let compiledAt = "${new Date().toString()}"` && ""
        return (formatTypescript.default as any)("R.ts", `namespace ${this.rootName}{\n` + Helper.indentForward(result) + `
${extra}
export const widgets = ${JSON.stringify(widgetPathes)}
\n}` + `
window["${this.rootName}"] = ${this.rootName}
`)
    }
    compileStyle(): string {
        return this.widgets.map(widget => widget.generateStyle()).filter(item => !!item).join("\n")
    }
    private getAllWidgetPath(widgets: WidgetTemplate[]): string[] {
        let result = []
        for (let child of widgets) {
            result.push(...child.getAllWidgetPath())
        }
        return result

    }
    private buildTree(widgets: WidgetTemplate[]): WidgetTree {
        let root: WidgetTree = {}
        for (let widget of widgets) {
            let names = widget.path.split("/").filter(item => !!item)
            let name = names.pop()
            let cur = root
            for (let folder of names) {
                if (root[folder]) cur = root[folder] as WidgetTree
                else cur = root[folder] = {}
                if (cur instanceof WidgetTemplate) {
                    throw new Error("Widget shouldn't have name confliction the the same level folder:" + folder)
                }
            }
            cur[name] = widget
        }
        return root
    }
    private recursiveGenerate(tree: WidgetTree): string {
        let result = ""
        for (let prop in tree) {
            let sub = tree[prop]
            if (sub instanceof WidgetTemplate) {
                result += sub.compile({ debug: this.debug })
            } else {
                let name = prop
                name = Leaf.Util.capitalize(name)
                result += `export namespace ${name}{`
                result += Helper.indentForward(this.recursiveGenerate(sub))
                result += "}"
            }
        }
        return result
    }
    generateNamespace(t: WidgetTemplate, holder: string = "WidgetContent"): string {
        // nested namespace
        let parts = pathModule.normalize(pathModule.dirname(t.path)).split("/").map(item => item.trim()).filter(item => !!item)
        let n: string
        let result = ``
        while (n = parts.pop()) {
            result = `export namespace ${n}{
${Helper.indentForward(result)} 
}`
        }
        return result
    }
}

interface WidgetTemplateData {
    name: string
    templateNodes: ElementStructure[]
    children: WidgetTemplateData[]
}

export class WidgetTemplate implements WidgetTemplateData {
    children: WidgetTemplate[] = []
    name: string
    parent: WidgetTemplate = null
    parser: WidgetStructureParserDesign
    protected error(arg: any, ...args: any[]) {
        console.error(arg, ...args)
    }
    public id: string = null
    public debugContext: DebugContext = null
    constructor(public readonly context: Context, public readonly templateNodes: ElementStructure[], public readonly path: string) {
        this.name = pathModule.basename(path)
        this.debugContext = {
            name: this.name,
            file: this.path,
            path: this.path
        }
        this.parser = new WidgetStructureParser(templateNodes, path, this.debugContext)
        this.id = CoreLogic.pathToId(this.path)
        for (let template of this.parser.templates) {
            if (!template.name) {
                this.error("template should have a name")
                continue
            }
            this.addChild(template.tag.children, template.name)
        }
    }
    getAllWidgetPath(): string[] {
        let path = [this.path]
        for (let child of this.children) {
            path.push(...child.getAllWidgetPath())
        }
        return path
    }
    addChild(tags: ElementStructure[], name: string) {
        let w = new WidgetTemplate(this.context, tags, pathModule.join(this.path, name))
        w.parent = this
        this.children.push(w)
    }
    public generateStyle() {
        let childStyles: string = this.children.map(item => item.generateStyle()).filter(item => !!item).join("\n")
        if (this.parser.styles.length === 0 && childStyles.length === 0) return ""
        let widgetStyle: string = ""
        if (this.parser.styles.length > 0) {
            widgetStyle = structureToString(this.parser.styles[0])
        }
        // child style should be at same level with parent, since every class are generated by a hash, so it's unlikely
        // to conflict. And if we don't nested we can preview the child alone
        return `.${this.id}{
${Helper.indentForward(widgetStyle)}
}
${childStyles}
`
    }
    public compile(option?: { debug?: boolean }): string {
        // 1. compile myself.
        // 2. compile children and put them into my namespace
        let WidgetName = Leaf.Util.capitalize(this.name)
        let UIInterfaces = "{" +
            this.parser.UIs.map(item => `${Leaf.Util.slugToCamel(item.name)}:${CoreLogic.getElementTypeStringByTagName(item.tagName)}`).join(",")
            + "}"
        let VMInterfaces = "{" + this.parser.VMs.map(item => `${item}:any`).join(",") + "}"
        let VMStringLiterals = this.parser.VMs.map(item => `"${item}"`).join("|") || "null"
        let Template = this.parser.getTemplateFunctionString()
        let VMStringArray = "[" + this.parser.VMs.map(item => `"${item}"`).join(",") + "]"
        let ListsDeclarations = this.parser.lists.map((list) => {
            if (list.reference) return `${list.name}:Leaf.List<${this.context.rootName}.${CoreLogic.pathToResourceResolution(list.reference)}>`
            return `${list.name}:Leaf.List<Leaf.WidgetAny>`
        }).join("\n")
        let WidgetsDeclarations = this.parser.widgets.filter(widget => !!widget.name).map((widget) => {
            if (widget.path) {
                return `${widget.name}:${this.context.rootName}.${CoreLogic.pathToResourceResolution(widget.path)}`
            }
            return `${widget.name}:Leaf.WidgetAny`
        }).join("\n")
        let Path = `"${this.path}"`
        let TestDatas = JSON.stringify(this.parser.testDatas)
        let InitialData = JSON.stringify(this.parser.initialData)
        let BindedLists = JSON.stringify(this.parser.lists)
        let BindedWidgets = JSON.stringify(this.parser.widgets)
        let RootName = this.context.rootName
        let self = Leaf.render(this.widgetCompileTemplate, {
            WidgetName, UIInterfaces, VMInterfaces, VMStringLiterals, Template, VMStringArray, ListsDeclarations, WidgetsDeclarations, Path, TestDatas, BindedLists, BindedWidgets, InitialData, RootName
        })
        if (this.children.length > 0) {
            self = self + `export namespace ${WidgetName}{`
                + Helper.indentForward(this.children.map(item => item.compile()).join("\n"))
                + "}"
        }
        return self
    }
    widgetCompileTemplate: string = `
export class {{WidgetName}} extends Leaf.GeneratedWidget<{{UIInterfaces}},{{VMInterfaces}},{{VMStringLiterals}}>{
    constructor() {
        super({{Template}}, "{{WidgetName}}",{{VMStringArray}})
        if({{WidgetName}}.InitialData){
            this.renderRecursive({{WidgetName}}.InitialData)
        }
    }
    {{ListsDeclarations}}
    {{WidgetsDeclarations}}
    static TestDatas = {{TestDatas}}
    static BindedLists = {{BindedLists}}
    static BindedWidgets = {{BindedWidgets}}
    static path = {{Path}}
    static InitialData = {{InitialData}}
    public TestDatas = {{WidgetName}}.TestDatas
    public BindedLists = {{WidgetName}}.BindedLists
    public BindedWidgets = {{WidgetName}}.BindedWidgets
    public InitialData = {{WidgetName}}.InitialData
    public path = {{WidgetName}}.path
    public GeneratedConstructor = {{WidgetName}}
    public R = {{RootName}}
}
({{WidgetName}}["prototype"] as any).name = "{{WidgetName}}"
`
}
abstract class WidgetStructureParserDesign {
    buildFunctionString: string
    public templates: {
        name: string
        tag: HTMLTag
    }[] = []
    public widgets: {
        path: string
        name: string
    }[] = []
    public UIs: {
        name: string
        tag: HTMLTag
        tagName: string
    }[] = []
    public VMs: string[] = []
    public slots: {
        name: string
        tag: HTMLTag
        index: number
    }[] = []
    public lists: {
        name: string
        type: "inline" | "reference" | "void"
        reference: string
    }[] = []
    public testDatas: {
        name: string
        content: string
        json: any
    }[] = []
    public initialData: any = null
    public scripts: HTMLTag[] = []
    public styles: HTMLTag[] = []
    public templateNodes: HTMLTag[] = []
    public rootWidgetNode: HTMLTag = null
    constructor(public readonly tags: ElementStructure[], public readonly path: string, public readonly debugContext: DebugContext) {
        this.parse(tags)
    }
    abstract parse(rootElements: ElementStructure[])
    abstract parseStructure(s: ElementStructure)
    abstract parseElement(tag: HTMLTag);
    abstract parseText(tag: HTMLText);
    abstract extractTemplates(tag: ElementStructure): { name: string, tag: HTMLTag }[];
    abstract getTemplateFunctionString(): string;
    protected abstract getDynamicProp(tag: HTMLTag): ElementConfigProperties
    protected abstract addUI(name: string, tag: HTMLTag);
    protected abstract addVM(name: string);
    protected abstract addVMByDynamicValueConfigs(configs: DynamicValueConfig[])
    protected abstract addTemplate(tag: HTMLTag, name?: string);
}
export class WidgetStructureParser extends WidgetStructureParserDesign {
    protected error(arg: any, ...args: any[]) {
        console.error(this.debugContext.file, ":", arg, ...args)
    }
    getTemplateFunctionString() {
        return `function(){
    let _e = this._e.bind(this)
    let _t = this._t.bind(this)
    return ${this.buildFunctionString}
} `
    }
    parse(rootElements: ElementStructure[]) {
        let scripts = []
        let styles = []
        let templates = []
        for (let item of rootElements) {
            if (item.type === "element") {
                let el = item as HTMLTag
                if (el.name === "script") {
                    scripts.push(el)
                    if (el.attrs["data-role"] == "test") {
                        let content = structureToString(el)
                        try {
                            this.testDatas.push({
                                name: el.attrs["data-name"] || `test-${this.testDatas.length + 1}`
                                , content: structureToString(content)
                                , json: Helper.parseLooseJSON(content)
                            })
                        } catch (e) {
                            this.error(content)
                            this.error("Fail to parse test data", e)
                        }
                    } else if (el.attrs["data-role"] == "init") {
                        let content = structureToString(el)
                        try {
                            this.initialData = Helper.parseLooseJSON(content)
                        }
                        catch (e) {
                            this.error(content)
                            this.error("Fail to parse init data", e)
                        }
                    }
                } else if (el.name === "style") {
                    styles.push(el)
                } else {
                    templates.push(el)
                }
            }
        }
        this.rootWidgetNode = templates[0]
        this.templateNodes = templates
        this.styles = styles
        this.scripts = scripts
        if (this.rootWidgetNode) {
            let className = this.rootWidgetNode.attrs["class"] || ""
            let classes = className.split(" ").filter(item => !!item)
            classes.unshift(CoreLogic.pathToId(this.path))
            this.rootWidgetNode.attrs["class"] = classes.join(" ")
            this.buildFunctionString = this.parseStructure(this.rootWidgetNode)
        }
    }
    parseStructure(s: ElementStructure) {
        if (s.type === "text") {
            return this.parseText(s as HTMLText)
        } else if (s.type === "element") {
            return this.parseElement(s as HTMLTag)
        }
    }
    parseText(text: HTMLText) {
        let fnName = "_t"
        return `${fnName}(${JSON.stringify(this.getDynamicStringConfig(text.content))})`
    }
    parseElement(tag: HTMLTag) {
        if (tag.name === "template") {
            this.addTemplate(tag)
            return null
        }
        if (tag.attrs["data-list"]) {
            let listName = tag.attrs["data-list"]
            let childName = tag.attrs["data-list-item"]
            let type: string, reference: string
            if (childName) {
                type = "reference"
                // /folder/parent ~ ./some/child => /folder/some/child
                reference = pathModule.join(this.path, "..", childName)
            } else {
                let templates = this.extractTemplates(tag)
                if (templates[0]) {
                    let child = templates[0]
                    if (!child.name) child.name = listName + "Item"
                    type = "inline"
                    // resolve to /parent/child
                    reference = pathModule.join(this.path, "/" + child.name)
                } else {
                    type = "void"
                    reference = null
                }
                this.templates.push(...templates)
            }
            this.lists.push({
                name: listName, type, reference
            } as any)
        }
        if (tag.attrs["data-id"]) {
            this.addUI(tag.attrs["data-id"], tag)
        }
        if (tag.name === "slot") {
            this.slots.push({
                name: tag.attrs["data-slot"],
                tag: tag,
                index: this.slots.length
            })
            if (tag.attrs["data-slot"]) {
                this.addUI(tag.attrs["data-slot"], tag)
            }
        }
        if (tag.name === "widget") {
            let path: string
            if (tag.attrs["data-widget"]) {
                path = pathModule.resolve(pathModule.dirname(this.path), tag.attrs["data-widget"])
            } else {
                path = null
            }
            let widget = {
                path: path,
                name: tag.attrs["data-name"] || CoreLogic.pathToName(tag.attrs["data-widget"] || "")
            }
            if (!widget.name) {
                this.error("widget requires a name")
            }
            this.widgets.push(widget)
        }
        let children = tag.children || []
        let childFunctionStrings = children.map(tag => this.parseStructure(tag)).filter(item => !!item)
        let fnName = "_e"
        return `${fnName}("${tag.name}", ${JSON.stringify(this.getDynamicProp(tag))},[${childFunctionStrings.join(",")}])`
    }
    protected getDynamicStringConfig(content: string) {
        let result = {
            templates: Leaf.CoreLogic.createDynamicValueConfigs(content)
        }
        this.addVMByDynamicValueConfigs(result.templates)
        return result
    }
    protected getDynamicProp(tag: HTMLTag): ElementConfigProperties {
        let tagName = tag.name
        let attr = tag.attrs
        let result: { [index: string]: DynamicPropertyConfig | string } = {}
        let temp: DynamicPropertyConfig = null
        for (let name in attr) {
            let value = attr[name]
            result[name] = temp = {
                name,
                templates: Leaf.CoreLogic.createDynamicValueConfigs(value)
            }
            this.addVMByDynamicValueConfigs(temp.templates)
        }
        return result
    }
    protected addUI(name: string, tag: HTMLTag) {
        for (let ui of this.UIs) {
            if (ui.name === name) {
                this.error("duplicate data-id", ui.name)
                return
            }
        }
        this.UIs.push({
            name, tag, tagName: tag.name
        })
    }
    protected addVM(name: string) {
        if (this.VMs.indexOf(name) >= 0) return
        this.VMs.push(name)
    }
    protected addVMByDynamicValueConfigs(configs: DynamicValueConfig[]) {
        for (let config of configs) {
            if (config.type === "value") {
                this.addVM(config.content)
            }
        }
    }
    protected addTemplate(tag: HTMLTag, name?: string) {
        name = tag.attrs["data-name"] || name
        if (!name) {
            this.error("template need a name")
            return
        }
        for (let template of this.templates) {
            if (template.name == name) {
                this.error(`duplicate template name ${name} `)
                return
            }
        }
        this.templates.push({ name, tag })
    }
    extractTemplates(e: ElementStructure): { name: string, tag: HTMLTag }[] {
        if (e.type === "text") return []
        let tag = e as HTMLTag
        let result = []
        if (!tag.children) tag.children = []
        tag.children = tag.children.filter((child: HTMLTag) => {
            if (child.name === "template") {
                result.push({ name: child.attrs["data-name"], tag: child })
                return false
            }
            result = result.concat(this.extractTemplates(child))
            return true
        })
        return result
    }
}
