import { CSSComputedStyleProperty } from "./css-agent";
import { InspectorEvents } from "./devtools-elements";

// Needed for typings only
import { ViewBase } from "../ui/core/view";

const registeredDomNodes = {};
const ELEMENT_NODE_TYPE = 1;
const ROOT_NODE_TYPE = 9;
const propertyBlacklist = [
    "effectivePaddingLeft",
    "effectivePaddingBottom",
    "effectivePaddingRight",
    "effectivePaddingTop",
    "effectiveBorderTopWidth",
    "effectiveBorderRightWidth",
    "effectiveBorderBottomWidth",
    "effectiveBorderLeftWidth",
    "effectiveMinWidth",
    "effectiveMinHeight",
    "effectiveWidth",
    "effectiveHeight",
    "effectiveMarginLeft",
    "effectiveMarginTop",
    "effectiveMarginRight",
    "effectiveMarginBottom",
    "nodeName",
    "nodeType",
    "decodeWidth",
    "decodeHeight",
    "ng-reflect-items",
    "domNode",
    "touchListenerIsSet",
    "bindingContext",
    "nativeView"
];

function lazy<T>(action: () => T): () => T {
    let _value: T;
    return () => _value || (_value = action());
}
const percentLengthToStringLazy = lazy<(length) => string>(() => require("../ui/styling/style-properties").PercentLength.convertToString);
const getSetPropertiesLazy = lazy<(view: ViewBase) => [string, any][]>(() => require("../ui/core/properties").getSetProperties);
const getComputedCssValuesLazy = lazy<(view: ViewBase) => [string, any][]>(() => require("../ui/core/properties").getComputedCssValues);

export function registerInspectorEvents(inspector: InspectorEvents) {
    inspectorFrontendInstance = inspector;
}

let inspectorFrontendInstance: any;
function notifyInspector(callback: (inspector: InspectorEvents) => void) {
    if (inspectorFrontendInstance) {
        callback(inspectorFrontendInstance);
    }
}

function valueToString(value: any): string {
    if (typeof value === "undefined" || value === null) {
        return "";
    } else if (typeof value === "object" && value.unit) {
        return percentLengthToStringLazy()(value);
    } else {
        return value + "";
    }
}

function propertyFilter([name, value]: [string, any]): boolean {
    if (name[0] === "_") {
        return false;
    }

    if (value !== null && typeof value === "object") {
        return false;
    }

    if (propertyBlacklist.indexOf(name) >= 0) {
        return false;
    }

    return true;
}

function registerNode(domNode: DOMNode) {
    registeredDomNodes[domNode.nodeId] = domNode;
}

function unregisterNode(domNode: DOMNode) {
    delete registeredDomNodes[domNode.nodeId];
}

export function getNodeById(id: number): DOMNode {
    return registeredDomNodes[id];
}

export class DOMNode {
    nodeId;
    nodeType;
    nodeName;
    localName;
    nodeValue = "";
    attributes: string[] = [];
    viewRef: WeakRef<ViewBase>;

    constructor(view: ViewBase) {
        this.viewRef = new WeakRef(view);
        this.nodeType = view.typeName === "Frame" ? ROOT_NODE_TYPE : ELEMENT_NODE_TYPE;
        this.nodeId = view._domId;
        this.nodeName = view.typeName;
        this.localName = this.nodeName;

        // Load all attributes
        this.loadAttributes();

        registerNode(this);
    }

    public loadAttributes() {
        this.attributes = [];
        getSetPropertiesLazy()(this.viewRef.get())
            .filter(propertyFilter)
            .forEach(pair => this.attributes.push(pair[0], pair[1] + ""));

    }

    get children(): DOMNode[] {
        const view = this.viewRef.get();
        if (!view) {
            return [];
        }

        const res = [];

        view.eachChild((child) => {
            child.ensureDomNode();
            res.push(child.domNode);
            return true;
        });

        return res;
    }

    onChildAdded(childView: ViewBase): void {
        notifyInspector((ins) => {
            const view = this.viewRef.get();

            let previousChild: ViewBase;
            view.eachChild((child) => {
                if (child === childView) {
                    return false;
                }

                previousChild = child;

                return true;
            });
            const index = !!previousChild ? previousChild._domId : 0;

            childView.ensureDomNode();
            ins.childNodeInserted(this.nodeId, index, childView.domNode);
        });
    }

    onChildRemoved(view: ViewBase): void {
        notifyInspector((ins) => {
            ins.childNodeRemoved(this.nodeId, view._domId);
        });
    }

    attributeModified(name: string, value: any) {
        notifyInspector((ins) => {
            if (propertyBlacklist.indexOf(name) < 0) {
                ins.attributeModified(this.nodeId, name, valueToString(value));
            }
        });
    }

    attributeRemoved(name: string) {
        notifyInspector((ins) => {
            ins.attributeRemoved(this.nodeId, name);
        });
    }

    getComputedProperties(): CSSComputedStyleProperty[] {
        const view = this.viewRef.get();
        if (!view) {
            return [];
        }

        const result = getComputedCssValuesLazy()(view)
            .filter(pair => pair[0][0] !== "_")
            .map((pair) => {
                return {
                    name: pair[0],
                    value: valueToString(pair[1])
                };
            });
        return result;
    }

    dispose() {
        unregisterNode(this);
        this.viewRef.clear();
    }

    public toObject() {
        return {
            nodeId: this.nodeId,
            nodeType: this.nodeType,
            nodeName: this.nodeName,
            localName: this.localName,
            nodeValue: this.nodeValue,
            children: this.children.map(c => c.toObject()),
            attributes: this.attributes
        };
    }
}
