// Types
import { unsetValue, Style, 
    CssProperty, CssAnimationProperty, 
    ShorthandProperty, InheritedCssProperty,
    makeValidator, makeParser } from "../core/properties";
import {
    Transformation,
    TransformationValue,
    TransformFunctionsInfo,
} from "../animation/animation";

import { dip, px, percent } from "../core/view";

import { Color } from "../../color";
import { Font, parseFont, FontStyle, FontWeight } from "../../ui/styling/font";
import { layout, hasDuplicates } from "../../utils/utils";
import { Background } from "../../ui/styling/background";
import { isIOS } from "../../platform";

import { radiansToDegrees } from "../../utils/number-utils";

import {
    decompose2DTransformMatrix,
    getTransformMatrix,
    matrixArrayToCssMatrix,
    multiplyAffine2d,
} from "../../matrix";

import * as parser from "../../css/parser";

export type LengthDipUnit = { readonly unit: "dip", readonly value: dip };
export type LengthPxUnit = { readonly unit: "px", readonly value: px };
export type LengthPercentUnit = { readonly unit: "%", readonly value: percent };

export type Length = "auto" | dip | LengthDipUnit | LengthPxUnit;
export type PercentLength = "auto" | dip | LengthDipUnit | LengthPxUnit | LengthPercentUnit;

function equalsCommon(a: Length, b: Length): boolean;
function equalsCommon(a: PercentLength, b: PercentLength): boolean;
function equalsCommon(a: PercentLength, b: PercentLength): boolean {
    if (a == "auto") { // tslint:disable-line
        return b == "auto";  // tslint:disable-line
    }
    if (typeof a === "number") {
        if (b == "auto") { // tslint:disable-line
            return false;
        }
        if (typeof b === "number") {
            return a == b; // tslint:disable-line
        }
        return b.unit == "dip" && a == b.value; // tslint:disable-line
    }
    if (b == "auto") { // tslint:disable-line
        return false;
    }
    if (typeof b === "number") {
        return a.unit == "dip" && a.value == b; // tslint:disable-line
    }
    return a.value == b.value && a.unit == b.unit; // tslint:disable-line
}

function convertToStringCommon(length: Length | PercentLength): string {
    if (length == "auto") { // tslint:disable-line
        return "auto";
    }

    if (typeof length === "number") {
        return length.toString();
    }

    let val = length.value;
    if (length.unit === "%") {
        val *= 100;
    }

    return val + length.unit;
}

function toDevicePixelsCommon(length: PercentLength, auto: number = Number.NaN, parentAvailableWidth: number = Number.NaN): number {
    if (length == "auto") { // tslint:disable-line
        return auto;
    }
    if (typeof length === "number") {
        return layout.round(layout.toDevicePixels(length));
    }
    switch (length.unit) {
        case "px":
            return layout.round(length.value);
        case "%":
            return layout.round(parentAvailableWidth * length.value);
        case "dip":
        default:
            return layout.round(layout.toDevicePixels(length.value));
    }
}

export namespace PercentLength {
    export function parse(fromValue: string | Length): PercentLength {
        if (fromValue == "auto") { // tslint:disable-line
            return "auto";
        }
        if (typeof fromValue === "string") {
            let stringValue = fromValue.trim();
            let percentIndex = stringValue.indexOf("%");
            if (percentIndex !== -1) {
                let value: percent;
                // if only % or % is not last we treat it as invalid value.
                if (percentIndex !== (stringValue.length - 1) || percentIndex === 0) {
                    value = Number.NaN;
                } else {
                    value = parseFloat(stringValue.substring(0, stringValue.length - 1).trim()) / 100;
                }

                if (isNaN(value) || !isFinite(value)) {
                    throw new Error(`Invalid value: ${fromValue}`);
                }
                return { unit: "%", value }
            } else if (stringValue.indexOf("px") !== -1) {
                stringValue = stringValue.replace("px", "").trim();
                let value: px = parseFloat(stringValue);
                if (isNaN(value) || !isFinite(value)) {
                    throw new Error(`Invalid value: ${fromValue}`);
                }
                return { unit: "px", value };
            } else {
                let value: dip = parseFloat(stringValue);
                if (isNaN(value) || !isFinite(value)) {
                    throw new Error(`Invalid value: ${fromValue}`);
                }
                return value;
            }
        } else {
            return fromValue;
        }
    }

    export const equals: { (a: PercentLength, b: PercentLength): boolean } = equalsCommon;
    export const toDevicePixels: { (length: PercentLength, auto: number, parentAvailableWidth: number): number } = toDevicePixelsCommon;
    export const convertToString: { (length: PercentLength): string } = convertToStringCommon;
}

export namespace Length {
    export function parse(fromValue: string | Length): Length {
        if (fromValue == "auto") { // tslint:disable-line
            return "auto";
        }
        if (typeof fromValue === "string") {
            let stringValue = fromValue.trim();
            if (stringValue.indexOf("px") !== -1) {
                stringValue = stringValue.replace("px", "").trim();
                let value: px = parseFloat(stringValue);
                if (isNaN(value) || !isFinite(value)) {
                    throw new Error(`Invalid value: ${stringValue}`);
                }
                return { unit: "px", value };
            } else {
                let value: dip = parseFloat(stringValue);
                if (isNaN(value) || !isFinite(value)) {
                    throw new Error(`Invalid value: ${stringValue}`);
                }
                return value;
            }
        } else {
            return fromValue;
        }
    }
    export const equals: { (a: Length, b: Length): boolean } = equalsCommon;
    export const toDevicePixels: { (length: Length, auto?: number): number } = toDevicePixelsCommon;
    export const convertToString: { (length: Length): string } = convertToStringCommon;
}

export const zeroLength: Length = { value: 0, unit: "px" };

export const minWidthProperty = new CssProperty<Style, Length>({
    name: "minWidth", cssName: "min-width", defaultValue: zeroLength, affectsLayout: isIOS, equalityComparer: Length.equals,
    valueChanged: (target, oldValue, newValue) => {
        target.view.effectiveMinWidth = Length.toDevicePixels(newValue, 0);
    }, valueConverter: Length.parse
});
minWidthProperty.register(Style);

export const minHeightProperty = new CssProperty<Style, Length>({
    name: "minHeight", cssName: "min-height", defaultValue: zeroLength, affectsLayout: isIOS, equalityComparer: Length.equals,
    valueChanged: (target, oldValue, newValue) => {
        target.view.effectiveMinHeight = Length.toDevicePixels(newValue, 0);
    }, valueConverter: Length.parse
});
minHeightProperty.register(Style);

export const widthProperty = new CssProperty<Style, PercentLength>({ name: "width", cssName: "width", defaultValue: "auto", affectsLayout: isIOS, equalityComparer: Length.equals, valueConverter: PercentLength.parse });
widthProperty.register(Style);

export const heightProperty = new CssProperty<Style, PercentLength>({ name: "height", cssName: "height", defaultValue: "auto", affectsLayout: isIOS, equalityComparer: Length.equals, valueConverter: PercentLength.parse });
heightProperty.register(Style);

const marginProperty = new ShorthandProperty<Style, string | PercentLength>({
    name: "margin", cssName: "margin",
    getter: function (this: Style) {
        if (PercentLength.equals(this.marginTop, this.marginRight) &&
            PercentLength.equals(this.marginTop, this.marginBottom) &&
            PercentLength.equals(this.marginTop, this.marginLeft)) {
            return this.marginTop;
        }
        return `${PercentLength.convertToString(this.marginTop)} ${PercentLength.convertToString(this.marginRight)} ${PercentLength.convertToString(this.marginBottom)} ${PercentLength.convertToString(this.marginLeft)}`;
    },
    converter: convertToMargins
});
marginProperty.register(Style);

export const marginLeftProperty = new CssProperty<Style, PercentLength>({ name: "marginLeft", cssName: "margin-left", defaultValue: zeroLength, affectsLayout: isIOS, equalityComparer: Length.equals, valueConverter: PercentLength.parse });
marginLeftProperty.register(Style);

export const marginRightProperty = new CssProperty<Style, PercentLength>({ name: "marginRight", cssName: "margin-right", defaultValue: zeroLength, affectsLayout: isIOS, equalityComparer: Length.equals, valueConverter: PercentLength.parse });
marginRightProperty.register(Style);

export const marginTopProperty = new CssProperty<Style, PercentLength>({ name: "marginTop", cssName: "margin-top", defaultValue: zeroLength, affectsLayout: isIOS, equalityComparer: Length.equals, valueConverter: PercentLength.parse });
marginTopProperty.register(Style);

export const marginBottomProperty = new CssProperty<Style, PercentLength>({ name: "marginBottom", cssName: "margin-bottom", defaultValue: zeroLength, affectsLayout: isIOS, equalityComparer: Length.equals, valueConverter: PercentLength.parse });
marginBottomProperty.register(Style);

const paddingProperty = new ShorthandProperty<Style, string | Length>({
    name: "padding", cssName: "padding",
    getter: function (this: Style) {
        if (Length.equals(this.paddingTop, this.paddingRight) &&
            Length.equals(this.paddingTop, this.paddingBottom) &&
            Length.equals(this.paddingTop, this.paddingLeft)) {
            return this.paddingTop;
        }
        return `${Length.convertToString(this.paddingTop)} ${Length.convertToString(this.paddingRight)} ${Length.convertToString(this.paddingBottom)} ${Length.convertToString(this.paddingLeft)}`;
    },
    converter: convertToPaddings
});
paddingProperty.register(Style);

export const paddingLeftProperty = new CssProperty<Style, Length>({
    name: "paddingLeft", cssName: "padding-left", defaultValue: zeroLength, affectsLayout: isIOS, equalityComparer: Length.equals,
    valueChanged: (target, oldValue, newValue) => {
        target.view.effectivePaddingLeft = Length.toDevicePixels(newValue, 0);
    }, valueConverter: Length.parse
});
paddingLeftProperty.register(Style);

export const paddingRightProperty = new CssProperty<Style, Length>({
    name: "paddingRight", cssName: "padding-right", defaultValue: zeroLength, affectsLayout: isIOS, equalityComparer: Length.equals,
    valueChanged: (target, oldValue, newValue) => {
        target.view.effectivePaddingRight = Length.toDevicePixels(newValue, 0);
    }, valueConverter: Length.parse
});
paddingRightProperty.register(Style);

export const paddingTopProperty = new CssProperty<Style, Length>({
    name: "paddingTop", cssName: "padding-top", defaultValue: zeroLength, affectsLayout: isIOS, equalityComparer: Length.equals,
    valueChanged: (target, oldValue, newValue) => {
        target.view.effectivePaddingTop = Length.toDevicePixels(newValue, 0);
    }, valueConverter: Length.parse
});
paddingTopProperty.register(Style);

export const paddingBottomProperty = new CssProperty<Style, Length>({
    name: "paddingBottom", cssName: "padding-bottom", defaultValue: zeroLength, affectsLayout: isIOS, equalityComparer: Length.equals,
    valueChanged: (target, oldValue, newValue) => {
        target.view.effectivePaddingBottom = Length.toDevicePixels(newValue, 0);
    }, valueConverter: Length.parse
});
paddingBottomProperty.register(Style);

export type HorizontalAlignment = "left" | "center" | "right" | "stretch";
export namespace HorizontalAlignment {
    export const LEFT: "left" = "left";
    export const CENTER: "center" = "center";
    export const RIGHT: "right" = "right";
    export const STRETCH: "stretch" = "stretch";
    export const isValid = makeValidator<HorizontalAlignment>(LEFT, CENTER, RIGHT, STRETCH);
    export const parse = makeParser<HorizontalAlignment>(isValid);
}

export const horizontalAlignmentProperty = new CssProperty<Style, HorizontalAlignment>({ name: "horizontalAlignment", cssName: "horizontal-align", defaultValue: HorizontalAlignment.STRETCH, affectsLayout: isIOS, valueConverter: HorizontalAlignment.parse });
horizontalAlignmentProperty.register(Style);

export type VerticalAlignment = "top" | "middle" | "bottom" | "stretch";
export namespace VerticalAlignment {
    export const TOP: "top" = "top";
    export const MIDDLE: "middle" = "middle";
    export const BOTTOM: "bottom" = "bottom";
    export const STRETCH: "stretch" = "stretch";
    export const isValid = makeValidator<VerticalAlignment>(TOP, MIDDLE, BOTTOM, STRETCH);
    export const parse = (value: string) => value.toLowerCase() === "center" ? MIDDLE : parseStrict(value);
    const parseStrict = makeParser<VerticalAlignment>(isValid);
}

export const verticalAlignmentProperty = new CssProperty<Style, VerticalAlignment>({ name: "verticalAlignment", cssName: "vertical-align", defaultValue: VerticalAlignment.STRETCH, affectsLayout: isIOS, valueConverter: VerticalAlignment.parse });
verticalAlignmentProperty.register(Style);

interface Thickness {
    top: string;
    right: string;
    bottom: string;
    left: string;
}

function parseThickness(value: string): Thickness {
    if (typeof value === "string") {
        let arr = value.split(/[ ,]+/);

        let top: string;
        let right: string;
        let bottom: string;
        let left: string;

        if (arr.length === 1) {
            top = arr[0];
            right = arr[0];
            bottom = arr[0];
            left = arr[0];
        }
        else if (arr.length === 2) {
            top = arr[0];
            bottom = arr[0];
            right = arr[1];
            left = arr[1];
        }
        else if (arr.length === 3) {
            top = arr[0];
            right = arr[1];
            left = arr[1];
            bottom = arr[2];
        }
        else if (arr.length === 4) {
            top = arr[0];
            right = arr[1];
            bottom = arr[2];
            left = arr[3];
        }
        else {
            throw new Error("Expected 1, 2, 3 or 4 parameters. Actual: " + value);
        }

        return {
            top: top,
            right: right,
            bottom: bottom,
            left: left
        };
    } else {
        return value;
    }
}

function convertToMargins(this: void, value: string | PercentLength): [CssProperty<any, any>, any][] {
    if (typeof value === "string" && value !== "auto") {
        let thickness = parseThickness(value);
        return [
            [marginTopProperty, PercentLength.parse(thickness.top)],
            [marginRightProperty, PercentLength.parse(thickness.right)],
            [marginBottomProperty, PercentLength.parse(thickness.bottom)],
            [marginLeftProperty, PercentLength.parse(thickness.left)]
        ];
    }
    else {
        return [
            [marginTopProperty, value],
            [marginRightProperty, value],
            [marginBottomProperty, value],
            [marginLeftProperty, value]
        ];
    }
}

function convertToPaddings(this: void, value: string | Length): [CssProperty<any, any>, any][] {
    if (typeof value === "string" && value !== "auto") {
        let thickness = parseThickness(value);
        return [
            [paddingTopProperty, Length.parse(thickness.top)],
            [paddingRightProperty, Length.parse(thickness.right)],
            [paddingBottomProperty, Length.parse(thickness.bottom)],
            [paddingLeftProperty, Length.parse(thickness.left)]
        ];
    }
    else {
        return [
            [paddingTopProperty, value],
            [paddingRightProperty, value],
            [paddingBottomProperty, value],
            [paddingLeftProperty, value]
        ];
    }
}

export const rotateProperty = new CssAnimationProperty<Style, number>({ name: "rotate", cssName: "rotate", defaultValue: 0, valueConverter: parseFloat });
rotateProperty.register(Style);

export const scaleXProperty = new CssAnimationProperty<Style, number>({ name: "scaleX", cssName: "scaleX", defaultValue: 1, valueConverter: parseFloat });
scaleXProperty.register(Style);

export const scaleYProperty = new CssAnimationProperty<Style, number>({ name: "scaleY", cssName: "scaleY", defaultValue: 1, valueConverter: parseFloat });
scaleYProperty.register(Style);

function parseDIPs(value: string): dip {
    if (value.indexOf("px") !== -1) {
        return layout.toDeviceIndependentPixels(parseFloat(value.replace("px", "").trim()));
    } else {
        return parseFloat(value.replace("dip", "").trim());
    }
}

export const translateXProperty = new CssAnimationProperty<Style, dip>({ name: "translateX", cssName: "translateX", defaultValue: 0, valueConverter: parseDIPs });
translateXProperty.register(Style);

export const translateYProperty = new CssAnimationProperty<Style, dip>({ name: "translateY", cssName: "translateY", defaultValue: 0, valueConverter: parseDIPs });
translateYProperty.register(Style);

const transformProperty = new ShorthandProperty<Style, string>({
    name: "transform", cssName: "transform",
    getter: function (this: Style) {
        let scaleX = this.scaleX;
        let scaleY = this.scaleY;
        let translateX = this.translateX;
        let translateY = this.translateY;
        let rotate = this.rotate;
        let result = "";
        if (translateX !== 0 || translateY !== 0) {
            result += `translate(${translateX}, ${translateY}) `;
        }
        if (scaleX !== 1 || scaleY !== 1) {
            result += `scale(${scaleX}, ${scaleY}) `;
        }
        if (rotate !== 0) {
            result += `rotate (${rotate})`;
        }

        return result.trim();
    },
    converter: convertToTransform
});
transformProperty.register(Style);

const IDENTITY_TRANSFORMATION = {
    translate: { x: 0, y: 0 },
    rotate: 0,
    scale: { x: 1, y: 1 },
};

const TRANSFORM_SPLITTER = new RegExp(/\s*(.+?)\((.*?)\)/g);
const TRANSFORMATIONS = Object.freeze([
    "rotate",
    "translate",
    "translate3d",
    "translateX",
    "translateY",
    "scale",
    "scale3d",
    "scaleX",
    "scaleY",
]);

const STYLE_TRANSFORMATION_MAP = Object.freeze({
    "scale": value => ({ property: "scale", value }),
    "scale3d": value => ({ property: "scale", value }),
    "scaleX": ({ x }) => ({ property: "scale", value: { x, y: IDENTITY_TRANSFORMATION.scale.y } }),
    "scaleY": ({ y }) => ({ property: "scale", value: { y, x: IDENTITY_TRANSFORMATION.scale.x } }),

    "translate": value => ({ property: "translate", value }),
    "translate3d": value => ({ property: "translate", value }),
    "translateX": ({ x }) => ({ property: "translate", value: { x, y: IDENTITY_TRANSFORMATION.translate.y } }),
    "translateY": ({ y }) => ({ property: "translate", value: { y, x: IDENTITY_TRANSFORMATION.translate.x } }),

    "rotate": value => ({ property: "rotate", value }),
});

function convertToTransform(value: string): [CssProperty<any, any>, any][] {
    if (value === unsetValue) {
        value = "none";
    }

    const { translate, rotate, scale } = transformConverter(value);
    return [
        [translateXProperty, translate.x],
        [translateYProperty, translate.y],

        [scaleXProperty, scale.x],
        [scaleYProperty, scale.y],

        [rotateProperty, rotate],
    ];
}

export function transformConverter(text: string): TransformFunctionsInfo {
    const transformations = parseTransformString(text);

    if (text === "none" || text === "" || !transformations.length) {
        return IDENTITY_TRANSFORMATION;
    }

    const usedTransforms = transformations.map(t => t.property);
    if (!hasDuplicates(usedTransforms)) {
        const fullTransformations = Object.assign({}, IDENTITY_TRANSFORMATION);
        transformations.forEach(transform => {
            fullTransformations[transform.property] = transform.value;
        });

        return fullTransformations;
    }

    const affineMatrix = transformations
        .map(getTransformMatrix)
        .reduce(multiplyAffine2d)
    const cssMatrix = matrixArrayToCssMatrix(affineMatrix)

    return decompose2DTransformMatrix(cssMatrix);
}

// using general regex and manually checking the matched
// properties is faster than using more specific regex
// https://jsperf.com/cssparse
function parseTransformString(text: string): Transformation[] {
    let matches: Transformation[] = [];
    let match;

    while ((match = TRANSFORM_SPLITTER.exec(text)) !== null) {
        const property = match[1];
        const value = convertTransformValue(property, match[2]);

        if (TRANSFORMATIONS.indexOf(property) !== -1) {
            matches.push(normalizeTransformation({ property, value }));
        }
    }

    return matches;
}

function normalizeTransformation({ property, value }: Transformation): Transformation {
    return <any>STYLE_TRANSFORMATION_MAP[property](value);
}

function convertTransformValue(property: string, stringValue: string)
    : TransformationValue {

    const [x, y = x] = stringValue.split(",").map(parseFloat);

    if (property === "rotate") {
        return stringValue.slice(-3) === "rad" ? radiansToDegrees(x) : x;
    }

    return { x, y };
}

// Background properties.
const backgroundProperty = new ShorthandProperty<Style, string | Color>({
    name: "background", cssName: "background",
    getter: function (this: Style) {
        return `${this.backgroundColor} ${this.backgroundImage} ${this.backgroundRepeat} ${this.backgroundPosition}`;
    },
    converter: convertToBackgrounds
});
backgroundProperty.register(Style);

export const backgroundInternalProperty = new CssProperty<Style, Background>({
    name: "backgroundInternal",
    cssName: "_backgroundInternal",
    defaultValue: Background.default
});
backgroundInternalProperty.register(Style);

// const pattern: RegExp = /url\(('|")(.*?)\1\)/;
export const backgroundImageProperty = new CssProperty<Style, string>({
    name: "backgroundImage", cssName: "background-image", valueChanged: (target, oldValue, newValue) => {
        const background = target.backgroundInternal.withImage(newValue);
        target.backgroundInternal = background;
    }
});
backgroundImageProperty.register(Style);

export const backgroundColorProperty = new CssAnimationProperty<Style, Color>({
    name: "backgroundColor", cssName: "background-color", valueChanged: (target, oldValue, newValue) => {
        const background = target.backgroundInternal.withColor(newValue);
        target.backgroundInternal = background;
    }, equalityComparer: Color.equals, valueConverter: (value) => new Color(value)
});
backgroundColorProperty.register(Style);

export type BackgroundRepeat = "repeat" | "repeat-x" | "repeat-y" | "no-repeat";
export namespace BackgroundRepeat {
    export const REPEAT: "repeat" = "repeat";
    export const REPEAT_X: "repeat-x" = "repeat-x";
    export const REPEAT_Y: "repeat-y" = "repeat-y";
    export const NO_REPEAT: "no-repeat" = "no-repeat";
    export const isValid = makeValidator<BackgroundRepeat>(REPEAT, REPEAT_X, REPEAT_Y, NO_REPEAT);
    export const parse = makeParser<BackgroundRepeat>(isValid);
}

export const backgroundRepeatProperty = new CssProperty<Style, BackgroundRepeat>({
    name: "backgroundRepeat", cssName: "background-repeat", valueConverter: BackgroundRepeat.parse,
    valueChanged: (target, oldValue, newValue) => {
        const background = target.backgroundInternal.withRepeat(newValue);
        target.backgroundInternal = background;
    }
});
backgroundRepeatProperty.register(Style);

export const backgroundSizeProperty = new CssProperty<Style, string>({
    name: "backgroundSize", cssName: "background-size", valueChanged: (target, oldValue, newValue) => {
        const background = target.backgroundInternal.withSize(newValue);
        target.backgroundInternal = background;
    }
});
backgroundSizeProperty.register(Style);

export const backgroundPositionProperty = new CssProperty<Style, string>({
    name: "backgroundPosition", cssName: "background-position", valueChanged: (target, oldValue, newValue) => {
        const background = target.backgroundInternal.withPosition(newValue);
        target.backgroundInternal = background;
    }
});
backgroundPositionProperty.register(Style);

function convertToBackgrounds(this: void, value: string): [CssProperty<any, any>, any][] {
    if (typeof value === "string") {
        const backgrounds = parser.parseBackground(value).value;
        const backgroundColor = backgrounds.color ? new Color(backgrounds.color) : unsetValue;
        const backgroundImage = backgrounds.image || unsetValue;
        const backgroundRepeat = backgrounds.repeat || unsetValue;
        const backgroundPosition = backgrounds.position ? backgrounds.position.text : unsetValue;

        return [
            [backgroundColorProperty, backgroundColor],
            [backgroundImageProperty, backgroundImage],
            [backgroundRepeatProperty, backgroundRepeat],
            [backgroundPositionProperty, backgroundPosition]
        ];
    } else {
        return [
            [backgroundColorProperty, unsetValue],
            [backgroundImageProperty, unsetValue],
            [backgroundRepeatProperty, unsetValue],
            [backgroundPositionProperty, unsetValue]
        ];
    }
}

function parseBorderColor(value: string): { top: Color, right: Color, bottom: Color, left: Color } {
    let result: { top: Color, right: Color, bottom: Color, left: Color } = { top: undefined, right: undefined, bottom: undefined, left: undefined };
    if (value.indexOf("rgb") === 0) {
        result.top = result.right = result.bottom = result.left = new Color(value);
        return result;
    }

    let arr = value.split(/[ ,]+/);
    if (arr.length === 1) {
        let arr0 = new Color(arr[0]);
        result.top = arr0;
        result.right = arr0;
        result.bottom = arr0;
        result.left = arr0;
    }
    else if (arr.length === 2) {
        let arr0 = new Color(arr[0]);
        let arr1 = new Color(arr[1]);
        result.top = arr0;
        result.right = arr1;
        result.bottom = arr0;
        result.left = arr1;
    }
    else if (arr.length === 3) {
        let arr0 = new Color(arr[0]);
        let arr1 = new Color(arr[1]);
        let arr2 = new Color(arr[2]);
        result.top = arr0;
        result.right = arr1;
        result.bottom = arr2;
        result.left = arr1;
    }
    else if (arr.length === 4) {
        let arr0 = new Color(arr[0]);
        let arr1 = new Color(arr[1]);
        let arr2 = new Color(arr[2]);
        let arr3 = new Color(arr[3]);
        result.top = arr0;
        result.right = arr1;
        result.bottom = arr2;
        result.left = arr3;
    }
    else {
        throw new Error(`Expected 1, 2, 3 or 4 parameters. Actual: ${value}`);
    }
    return result;
}

// Border Color properties.
const borderColorProperty = new ShorthandProperty<Style, string | Color>({
    name: "borderColor", cssName: "border-color",
    getter: function (this: Style) {
        if (Color.equals(this.borderTopColor, this.borderRightColor) &&
            Color.equals(this.borderTopColor, this.borderBottomColor) &&
            Color.equals(this.borderTopColor, this.borderLeftColor)) {
            return this.borderTopColor;
        }
        else {
            return `${this.borderTopColor} ${this.borderRightColor} ${this.borderBottomColor} ${this.borderLeftColor}`;
        }
    },
    converter: function (value) {
        if (typeof value === "string") {
            let fourColors = parseBorderColor(value);
            return [
                [borderTopColorProperty, fourColors.top],
                [borderRightColorProperty, fourColors.right],
                [borderBottomColorProperty, fourColors.bottom],
                [borderLeftColorProperty, fourColors.left]
            ];
        }
        else {
            return [
                [borderTopColorProperty, value],
                [borderRightColorProperty, value],
                [borderBottomColorProperty, value],
                [borderLeftColorProperty, value]
            ];
        }
    }
});
borderColorProperty.register(Style);

export const borderTopColorProperty = new CssProperty<Style, Color>({
    name: "borderTopColor", cssName: "border-top-color", valueChanged: (target, oldValue, newValue) => {
        const background = target.backgroundInternal.withBorderTopColor(newValue);
        target.backgroundInternal = background;
    }, equalityComparer: Color.equals, valueConverter: (value) => new Color(value)
});
borderTopColorProperty.register(Style);

export const borderRightColorProperty = new CssProperty<Style, Color>({
    name: "borderRightColor", cssName: "border-right-color", valueChanged: (target, oldValue, newValue) => {
        const background = target.backgroundInternal.withBorderRightColor(newValue);
        target.backgroundInternal = background;
    }, equalityComparer: Color.equals, valueConverter: (value) => new Color(value)
});
borderRightColorProperty.register(Style);

export const borderBottomColorProperty = new CssProperty<Style, Color>({
    name: "borderBottomColor", cssName: "border-bottom-color", valueChanged: (target, oldValue, newValue) => {
        const background = target.backgroundInternal.withBorderBottomColor(newValue);
        target.backgroundInternal = background;
    }, equalityComparer: Color.equals, valueConverter: (value) => new Color(value)
});
borderBottomColorProperty.register(Style);

export const borderLeftColorProperty = new CssProperty<Style, Color>({
    name: "borderLeftColor", cssName: "border-left-color", valueChanged: (target, oldValue, newValue) => {
        const background = target.backgroundInternal.withBorderLeftColor(newValue);
        target.backgroundInternal = background;
    }, equalityComparer: Color.equals, valueConverter: (value) => new Color(value)
});
borderLeftColorProperty.register(Style);

// Border Width properties.
const borderWidthProperty = new ShorthandProperty<Style, string | Length>({
    name: "borderWidth", cssName: "border-width",
    getter: function (this: Style) {
        if (Length.equals(this.borderTopWidth, this.borderRightWidth) &&
            Length.equals(this.borderTopWidth, this.borderBottomWidth) &&
            Length.equals(this.borderTopWidth, this.borderLeftWidth)) {
            return this.borderTopWidth;
        }
        else {
            return `${Length.convertToString(this.borderTopWidth)} ${Length.convertToString(this.borderRightWidth)} ${Length.convertToString(this.borderBottomWidth)} ${Length.convertToString(this.borderLeftWidth)}`;
        }
    },
    converter: function (value) {
        if (typeof value === "string" && value !== "auto") {
            let borderWidths = parseThickness(value);
            return [
                [borderTopWidthProperty, borderWidths.top],
                [borderRightWidthProperty, borderWidths.right],
                [borderBottomWidthProperty, borderWidths.bottom],
                [borderLeftWidthProperty, borderWidths.left]
            ];
        }
        else {
            return [
                [borderTopWidthProperty, value],
                [borderRightWidthProperty, value],
                [borderBottomWidthProperty, value],
                [borderLeftWidthProperty, value]
            ];
        }
    }
});
borderWidthProperty.register(Style);

export const borderTopWidthProperty = new CssProperty<Style, Length>({
    name: "borderTopWidth", cssName: "border-top-width", defaultValue: zeroLength, affectsLayout: isIOS, equalityComparer: Length.equals,
    valueChanged: (target, oldValue, newValue) => {
        let value = Length.toDevicePixels(newValue, 0);
        if (!isNonNegativeFiniteNumber(value)) {
            throw new Error(`border-top-width should be Non-Negative Finite number. Value: ${value}`);
        }

        target.view.effectiveBorderTopWidth = value;
        const background = target.backgroundInternal.withBorderTopWidth(value);
        target.backgroundInternal = background;
    }, valueConverter: Length.parse
});
borderTopWidthProperty.register(Style);

export const borderRightWidthProperty = new CssProperty<Style, Length>({
    name: "borderRightWidth", cssName: "border-right-width", defaultValue: zeroLength, affectsLayout: isIOS, equalityComparer: Length.equals,
    valueChanged: (target, oldValue, newValue) => {
        let value = Length.toDevicePixels(newValue, 0);
        if (!isNonNegativeFiniteNumber(value)) {
            throw new Error(`border-right-width should be Non-Negative Finite number. Value: ${value}`);
        }

        target.view.effectiveBorderRightWidth = value;
        const background = target.backgroundInternal.withBorderRightWidth(value);
        target.backgroundInternal = background;
    }, valueConverter: Length.parse
});
borderRightWidthProperty.register(Style);

export const borderBottomWidthProperty = new CssProperty<Style, Length>({
    name: "borderBottomWidth", cssName: "border-bottom-width", defaultValue: zeroLength, affectsLayout: isIOS, equalityComparer: Length.equals,
    valueChanged: (target, oldValue, newValue) => {
        let value = Length.toDevicePixels(newValue, 0);
        if (!isNonNegativeFiniteNumber(value)) {
            throw new Error(`border-bottom-width should be Non-Negative Finite number. Value: ${value}`);
        }

        target.view.effectiveBorderBottomWidth = value;
        const background = target.backgroundInternal.withBorderBottomWidth(value);
        target.backgroundInternal = background;
    }, valueConverter: Length.parse
});
borderBottomWidthProperty.register(Style);

export const borderLeftWidthProperty = new CssProperty<Style, Length>({
    name: "borderLeftWidth", cssName: "border-left-width", defaultValue: zeroLength, affectsLayout: isIOS, equalityComparer: Length.equals,
    valueChanged: (target, oldValue, newValue) => {
        let value = Length.toDevicePixels(newValue, 0);
        if (!isNonNegativeFiniteNumber(value)) {
            throw new Error(`border-left-width should be Non-Negative Finite number. Value: ${value}`);
        }

        target.view.effectiveBorderLeftWidth = value;
        const background = target.backgroundInternal.withBorderLeftWidth(value);
        target.backgroundInternal = background;
    }, valueConverter: Length.parse
});
borderLeftWidthProperty.register(Style);

// Border Radius properties.
const borderRadiusProperty = new ShorthandProperty<Style, string | Length>({
    name: "borderRadius", cssName: "border-radius",
    getter: function (this: Style) {
        if (Length.equals(this.borderTopLeftRadius, this.borderTopRightRadius) &&
            Length.equals(this.borderTopLeftRadius, this.borderBottomRightRadius) &&
            Length.equals(this.borderTopLeftRadius, this.borderBottomLeftRadius)) {
            return this.borderTopLeftRadius;
        }
        return `${Length.convertToString(this.borderTopLeftRadius)} ${Length.convertToString(this.borderTopRightRadius)} ${Length.convertToString(this.borderBottomRightRadius)} ${Length.convertToString(this.borderBottomLeftRadius)}`;
    },
    converter: function (value) {
        if (typeof value === "string") {
            let borderRadius = parseThickness(value);
            return [
                [borderTopLeftRadiusProperty, borderRadius.top],
                [borderTopRightRadiusProperty, borderRadius.right],
                [borderBottomRightRadiusProperty, borderRadius.bottom],
                [borderBottomLeftRadiusProperty, borderRadius.left]
            ];
        }
        else {
            return [
                [borderTopLeftRadiusProperty, value],
                [borderTopRightRadiusProperty, value],
                [borderBottomRightRadiusProperty, value],
                [borderBottomLeftRadiusProperty, value]
            ];
        }
    }
});
borderRadiusProperty.register(Style);

export const borderTopLeftRadiusProperty = new CssProperty<Style, Length>({
    name: "borderTopLeftRadius", cssName: "border-top-left-radius", defaultValue: 0, affectsLayout: isIOS, valueChanged: (target, oldValue, newValue) => {
        let value = Length.toDevicePixels(newValue, 0);
        if (!isNonNegativeFiniteNumber(value)) {
            throw new Error(`border-top-left-radius should be Non-Negative Finite number. Value: ${value}`);
        }
        const background = target.backgroundInternal.withBorderTopLeftRadius(value);
        target.backgroundInternal = background;
    }, valueConverter: Length.parse
});
borderTopLeftRadiusProperty.register(Style);

export const borderTopRightRadiusProperty = new CssProperty<Style, Length>({
    name: "borderTopRightRadius", cssName: "border-top-right-radius", defaultValue: 0, affectsLayout: isIOS, valueChanged: (target, oldValue, newValue) => {
        let value = Length.toDevicePixels(newValue, 0);
        if (!isNonNegativeFiniteNumber(value)) {
            throw new Error(`border-top-right-radius should be Non-Negative Finite number. Value: ${value}`);
        }
        const background = target.backgroundInternal.withBorderTopRightRadius(value);
        target.backgroundInternal = background;
    }, valueConverter: Length.parse
});
borderTopRightRadiusProperty.register(Style);

export const borderBottomRightRadiusProperty = new CssProperty<Style, Length>({
    name: "borderBottomRightRadius", cssName: "border-bottom-right-radius", defaultValue: 0, affectsLayout: isIOS, valueChanged: (target, oldValue, newValue) => {
        let value = Length.toDevicePixels(newValue, 0);
        if (!isNonNegativeFiniteNumber(value)) {
            throw new Error(`border-bottom-right-radius should be Non-Negative Finite number. Value: ${value}`);
        }
        const background = target.backgroundInternal.withBorderBottomRightRadius(value);
        target.backgroundInternal = background;
    }, valueConverter: Length.parse
});
borderBottomRightRadiusProperty.register(Style);

export const borderBottomLeftRadiusProperty = new CssProperty<Style, Length>({
    name: "borderBottomLeftRadius", cssName: "border-bottom-left-radius", defaultValue: 0, affectsLayout: isIOS, valueChanged: (target, oldValue, newValue) => {
        let value = Length.toDevicePixels(newValue, 0);
        if (!isNonNegativeFiniteNumber(value)) {
            throw new Error(`border-bottom-left-radius should be Non-Negative Finite number. Value: ${value}`);
        }
        const background = target.backgroundInternal.withBorderBottomLeftRadius(value);
        target.backgroundInternal = background;
    }, valueConverter: Length.parse
});
borderBottomLeftRadiusProperty.register(Style);

function isNonNegativeFiniteNumber(value: number): boolean {
    return isFinite(value) && !isNaN(value) && value >= 0;
}

let supportedPaths = ["rect", "circle", "ellipse", "polygon", "inset"];
function isClipPathValid(value: string): boolean {
    if (!value) {
        return true;
    }
    let functionName = value.substring(0, value.indexOf("(")).trim();
    return supportedPaths.indexOf(functionName) !== -1;
}

export const clipPathProperty = new CssProperty<Style, string>({
    name: "clipPath", cssName: "clip-path", valueChanged: (target, oldValue, newValue) => {
        if (!isClipPathValid(newValue)) {
            throw new Error("clip-path is not valid.");
        }

        const background = target.backgroundInternal.withClipPath(newValue);
        target.backgroundInternal = background;
    }
});
clipPathProperty.register(Style);

function isFloatValueConverter(value: string): number {
    let newValue = parseFloat(value);
    if (isNaN(newValue)) {
        throw new Error(`Invalid value: ${newValue}`);
    }

    return newValue;
}

export const zIndexProperty = new CssProperty<Style, number>({ name: "zIndex", cssName: "z-index", valueConverter: isFloatValueConverter });
zIndexProperty.register(Style);

function opacityConverter(value: any): number {
    let newValue = parseFloat(value);
    if (!isNaN(newValue) && 0 <= newValue && newValue <= 1) {
        return newValue;
    }

    throw new Error(`Opacity should be between [0, 1]. Value: ${newValue}`);
}

export const opacityProperty = new CssAnimationProperty<Style, number>({ name: "opacity", cssName: "opacity", defaultValue: 1, valueConverter: opacityConverter });
opacityProperty.register(Style);

export const colorProperty = new InheritedCssProperty<Style, Color>({ name: "color", cssName: "color", equalityComparer: Color.equals, valueConverter: (v) => new Color(v) });
colorProperty.register(Style);

export const fontInternalProperty = new CssProperty<Style, Font>({ name: "fontInternal", cssName: "_fontInternal", defaultValue: Font.default });
fontInternalProperty.register(Style);

export const fontFamilyProperty = new InheritedCssProperty<Style, string>({
    name: "fontFamily", cssName: "font-family", affectsLayout: isIOS, valueChanged: (target, oldValue, newValue) => {
        let currentFont = target.fontInternal;
        if (currentFont.fontFamily !== newValue) {
            const newFont = currentFont.withFontFamily(newValue);
            target.fontInternal = Font.equals(Font.default, newFont) ? unsetValue : newFont;
        }
    }
});
fontFamilyProperty.register(Style);

export const fontSizeProperty = new InheritedCssProperty<Style, number>({
    name: "fontSize", cssName: "font-size", affectsLayout: isIOS, valueChanged: (target, oldValue, newValue) => {
        let currentFont = target.fontInternal;
        if (currentFont.fontSize !== newValue) {
            const newFont = currentFont.withFontSize(newValue);
            target.fontInternal = Font.equals(Font.default, newFont) ? unsetValue : newFont;
        }
    },
    valueConverter: (v) => parseFloat(v)
});
fontSizeProperty.register(Style);

export const fontStyleProperty = new InheritedCssProperty<Style, FontStyle>({
    name: "fontStyle", cssName: "font-style", affectsLayout: isIOS, defaultValue: FontStyle.NORMAL, valueConverter: FontStyle.parse, valueChanged: (target, oldValue, newValue) => {
        let currentFont = target.fontInternal;
        if (currentFont.fontStyle !== newValue) {
            const newFont = currentFont.withFontStyle(newValue);
            target.fontInternal = Font.equals(Font.default, newFont) ? unsetValue : newFont;
        }
    }
});
fontStyleProperty.register(Style);

export const fontWeightProperty = new InheritedCssProperty<Style, FontWeight>({
    name: "fontWeight", cssName: "font-weight", affectsLayout: isIOS, defaultValue: FontWeight.NORMAL, valueConverter: FontWeight.parse, valueChanged: (target, oldValue, newValue) => {
        let currentFont = target.fontInternal;
        if (currentFont.fontWeight !== newValue) {
            const newFont = currentFont.withFontWeight(newValue);
            target.fontInternal = Font.equals(Font.default, newFont) ? unsetValue : newFont;
        }
    }
});
fontWeightProperty.register(Style);

const fontProperty = new ShorthandProperty<Style, string>({
    name: "font", cssName: "font",
    getter: function (this: Style) {
        return `${this.fontStyle} ${this.fontWeight} ${this.fontSize} ${this.fontFamily}`;
    },
    converter: function (value) {
        if (value === unsetValue) {
            return [
                [fontStyleProperty, unsetValue],
                [fontWeightProperty, unsetValue],
                [fontSizeProperty, unsetValue],
                [fontFamilyProperty, unsetValue]
            ];
        } else {
            const font = parseFont(value);
            const fontSize = parseFloat(font.fontSize);

            return [
                [fontStyleProperty, font.fontStyle],
                [fontWeightProperty, font.fontWeight],
                [fontSizeProperty, fontSize],
                [fontFamilyProperty, font.fontFamily]
            ];
        }
    }
});
fontProperty.register(Style);

export type Visibility = "visible" | "hidden" | "collapse";
export namespace Visibility {
    export const VISIBLE: "visible" = "visible";
    export const HIDDEN: "hidden" = "hidden";
    export const COLLAPSE: "collapse" = "collapse";
    export const isValid = makeValidator<Visibility>(VISIBLE, HIDDEN, COLLAPSE);
    export const parse = (value: string) => value.toLowerCase() === "collapsed" ? COLLAPSE : parseStrict(value);
    const parseStrict = makeParser<Visibility>(isValid);
}

export const visibilityProperty = new CssProperty<Style, Visibility>({
    name: "visibility", cssName: "visibility", defaultValue: Visibility.VISIBLE, affectsLayout: isIOS, valueConverter: Visibility.parse, valueChanged: (target, oldValue, newValue) => {
        target.view.isCollapsed = (newValue === Visibility.COLLAPSE);
    }
});
visibilityProperty.register(Style);