import view = require("ui/core/view");
import observable = require("ui/core/dependency-observable");
import cssParser = require("css");
import styleProperty = require("ui/styling/style-property");
import trace = require("trace");

var ID_SPECIFICITY = 10000;
var CLASS_SPECIFICITY = 100;
var TYPE_SPECIFICITY = 1;

export class CssSelector {
    private _expression: string;
    private _declarations: cssParser.Declaration[];

    constructor(expression: string, declarations: cssParser.Declaration[]) {
        this._expression = expression;
        this._declarations = declarations;
    }

    get expression(): string {
        return this._expression;
    }

    get declarations(): Array<{ property: string; value: any }> {
        return this._declarations;
    }

    get specificity(): number {
        throw "Specificity property is abstract";
    }

    public matches(view: view.View): boolean {
        return false;
    }

    public apply(view: view.View) {
        this.eachSetter((property, value) => {
            try {
                view.style._setValue(property, value, observable.ValueSource.Css);
            }
            catch (ex) {
                trace.write("Error setting property: " + property.name + " view: " + view + " value: " + value + " " + ex, trace.categories.Style, trace.messageType.error);
            }
        });
    }

    public eachSetter(callback: (property: styleProperty.Property, resolvedValue: any) => void) {
        for (let i = 0; i < this._declarations.length; i++) {
            let declaration = this._declarations[i];
            let name = declaration.property;
            let resolvedValue = declaration.value;
            let property = styleProperty.getPropertyByCssName(name);

            if (property) {                
                // The property.valueConverter is now used to convert the value later on in DependencyObservable._setValueInternal.
                callback(property, resolvedValue);
            }
            else {
                var pairs = styleProperty.getShorthandPairs(name, resolvedValue);
                if (pairs) {
                    for (let j = 0; j < pairs.length; j++) {
                        let pair = pairs[j];
                        callback(pair.property, pair.value);
                    }
                }
            }
        }
    }
}

class CssTypeSelector extends CssSelector {
    get specificity(): number {
        return TYPE_SPECIFICITY;
    }
    public matches(view: view.View): boolean {
        return matchesType(this.expression, view);
    }
}

function matchesType(expression: string, view: view.View): boolean {
    return expression.toLowerCase() === view.typeName.toLowerCase() ||
        expression.toLowerCase() === view.typeName.split(/(?=[A-Z])/).join("-").toLowerCase();
}

class CssIdSelector extends CssSelector {
    get specificity(): number {
        return ID_SPECIFICITY;
    }
    public matches(view: view.View): boolean {
        return this.expression === view.id;
    }
}

class CssClassSelector extends CssSelector {
    get specificity(): number {
        return CLASS_SPECIFICITY;
    }
    public matches(view: view.View): boolean {
        var expectedClass = this.expression;
        return view._cssClasses.some((cssClass, i, arr) => { return cssClass === expectedClass });
    }
}

export class CssVisualStateSelector extends CssSelector {
    private _key: string;
    private _match: string;
    private _state: string;
    private _isById: boolean;
    private _isByClass: boolean;
    private _isByType: boolean;

    get specificity(): number {
        return (this._isById ? ID_SPECIFICITY : 0) +
            (this._isByClass ? CLASS_SPECIFICITY : 0) +
            (this._isByType ? TYPE_SPECIFICITY : 0);
    }

    get key(): string {
        return this._key;
    }

    get state(): string {
        return this._state;
    }

    constructor(expression: string, declarations: cssParser.Declaration[]) {
        super(expression, declarations);

        var args = expression.split(COLON);
        this._key = args[0];
        this._state = args[1];

        if (this._key.charAt(0) === AMP) {
            this._match = this._key.substring(1);
            this._isById = true;
        } else if (this._key.charAt(0) === DOT) {
            this._match = this._key.substring(1);
            this._isByClass = true;
        }
        else if (this._key.length > 0) { // handle the case when there is no key. E.x. ":pressed" selector
            this._match = this._key;
            this._isByType = true;
        }
    }

    public matches(view: view.View): boolean {
        var matches = true;
        if (this._isById) {
            matches = this._match === view.id;
        }

        if (this._isByClass) {
            var expectedClass = this._match;
            matches = view._cssClasses.some((cssClass, i, arr) => { return cssClass === expectedClass });
        }

        if (this._isByType) {
            matches = matchesType(this._match, view);
        }

        return matches;
    }
}

var AMP = "#",
    DOT = ".",
    COLON = ":";

export function createSelector(expression: string, declarations: cssParser.Declaration[]): CssSelector {
    var colonIndex = expression.indexOf(COLON);
    if (colonIndex >= 0) {
        return new CssVisualStateSelector(expression, declarations);
    }

    if (expression.charAt(0) === AMP) {
        return new CssIdSelector(expression.substring(1), declarations);
    }

    if (expression.charAt(0) === DOT) {
        // TODO: Combinations like label.center
        return new CssClassSelector(expression.substring(1), declarations);
    }

    return new CssTypeSelector(expression, declarations);
}

class InlineStyleSelector extends CssSelector {
    constructor(declarations: cssParser.Declaration[]) {
        super(undefined, declarations)
    }

    public apply(view: view.View) {
        this.eachSetter((property, value) => {
            view.style._setValue(property, value, observable.ValueSource.Local);
        });
    }
}

export function applyInlineSyle(view: view.View, declarations: cssParser.Declaration[]) {
    var localStyleSelector = new InlineStyleSelector(declarations);
    localStyleSelector.apply(view);
}
