import application = require("application");
import styling = require("ui/styling");
import types = require("utils/types");
import trace = require("trace");
import {DependencyObservable, PropertyMetadata, PropertyMetadataSettings, PropertyChangeData, Property, ValueSource, NativeValueResult} from "ui/core/dependency-observable";
import {View} from "ui/core/view";
import {Color} from "color";
import styleProperty = require("ui/styling/style-property");
import converters = require("./converters");
import enums = require("ui/enums");
import utils = require("utils/utils");
import font = require("ui/styling/font");
import background = require("ui/styling/background");
import platform = require("platform");
import definition = require("ui/styling/style");
import * as imageSourceModule from "image-source";

var imageSource: typeof imageSourceModule;
function ensureImageSource() {
    if (!imageSource) {
        imageSource = require("image-source");
    }
}

// key is the property id and value is Dictionary<string, StylePropertyChangedHandler>;
var _registeredHandlers = Array<Object>();

// key is a className + property id and value is StylePropertyChangedHandler;
var _handlersCache = {};

// classes like Frame that does not need to handle styling properties.
var noStylingClasses = {};

// on Android we explicitly set propertySettings to None because android will invalidate its layout (skip unnecessary native call).
var AffectsLayout = platform.device.os === platform.platformNames.android ? PropertyMetadataSettings.None : PropertyMetadataSettings.AffectsLayout;

interface ThicknessValue {
    left: Object;
    top: Object;
    right: Object;
    bottom: Object;
}

interface PercentHelper {
    value: number;
    isPercent: boolean;
    isError: boolean;
}

function parseMargin(value: any): ThicknessValue {
    if (types.isString(value)) {
        let arr = (<string>value).split(/[ ,]+/);

        let top: Object;
        let right: Object;
        let bottom: Object;
        let left: Object;

        if (arr.length === 1) {
            top = right = bottom = left = arr[0];
        }
        else if (arr.length === 2) {
            top = bottom = arr[0];
            right = left = arr[1];
        }
        else if (arr.length === 4) {
            top = arr[0];
            right = arr[1];
            bottom = arr[2];
            left = arr[3];
        }
        else {
            throw new Error("Invalid value for margin: " + value);
        }

        return {
            top: top,
            right: right,
            bottom: bottom,
            left: left
        }
    }
    else if (types.isNumber(value)) {
        return {
            top: value,
            right: value,
            bottom: value,
            left: value
        }
    }
    else {
        return value;
    }
}

function parseThickness(value: any): definition.Thickness {
    var result: definition.Thickness = { top: 0, right: 0, bottom: 0, left: 0 };
    if (types.isString(value)) {
        var arr = value.split(/[ ,]+/);
        var top = parseInt(arr[0]);
        top = isNaN(top) ? 0 : top;

        var right = parseInt(arr[1]);
        right = isNaN(right) ? top : right;

        var bottom = parseInt(arr[2]);
        bottom = isNaN(bottom) ? top : bottom;

        var left = parseInt(arr[3]);
        left = isNaN(left) ? right : left;

        result.top = top;
        result.right = right;
        result.bottom = bottom;
        result.left = left;

    } else if (types.isNumber(value)) {
        result.top = result.right = result.bottom = result.left = value;
    }
    else {
        result = value;
    }

    return result;
}

function layoutParamsComparer(x: definition.CommonLayoutParams, y: definition.CommonLayoutParams): boolean {
    return x.width === y.width
        && x.height === y.height
        && x.leftMargin === y.leftMargin
        && x.topMargin === y.topMargin
        && x.rightMargin === y.rightMargin
        && x.bottomMargin === y.bottomMargin
        && x.horizontalAlignment === y.horizontalAlignment
        && x.verticalAlignment === y.verticalAlignment
        && x.widthPercent === y.widthPercent
        && x.heightPercent === y.heightPercent
        && x.leftMarginPercent === y.leftMarginPercent
        && x.topMarginPercent === y.topMarginPercent
        && x.rightMarginPercent === y.rightMarginPercent
        && x.bottomMarginPercent === y.bottomMarginPercent
}

function onLayoutParamsChanged(data: PropertyChangeData) {
    let style = <Style>data.object;
    let widthValue = convertToPercentHelper(style.width);

    let width: number;
    let widthPercent: number;
    if (widthValue.isPercent) {
        width = style.horizontalAlignment === enums.HorizontalAlignment.stretch ? -1 : -2;
        widthPercent = widthValue.value / 100;
    }
    else {
        width = isNaN(widthValue.value) ? -1 : widthValue.value;
        widthPercent = -1;
    }

    let heightValue = convertToPercentHelper(style.height);
    let height: number;
    let heightPercent: number;
    if (heightValue.isPercent) {
        height = style.verticalAlignment === enums.VerticalAlignment.stretch ? -1 : -2;
        heightPercent = heightValue.value / 100;
    }
    else {
        height = isNaN(heightValue.value) ? -1 : heightValue.value;
        heightPercent = -1;
    }

    let marginLeftValue = convertToPercentHelper(style.marginLeft);
    let marginTopValue = convertToPercentHelper(style.marginTop);
    let marginRightValue = convertToPercentHelper(style.marginRight);
    let marginBottomValue = convertToPercentHelper(style.marginBottom);

    // Negative marginPercent means no marginPercent so native layout won't override margin with this % value.
    var layoutParams: definition.CommonLayoutParams =
        {
            width: width,
            height: height,
            widthPercent: widthPercent,
            heightPercent: heightPercent,
            leftMargin: marginLeftValue.isPercent ? 0 : marginLeftValue.value,
            leftMarginPercent: marginLeftValue.isPercent ? marginLeftValue.value / 100 : -1,
            topMargin: marginTopValue.isPercent ? 0 : marginTopValue.value,
            topMarginPercent: marginTopValue.isPercent ? marginTopValue.value / 100 : -1,
            rightMargin: marginRightValue.isPercent ? 0 : marginRightValue.value,
            rightMarginPercent: marginRightValue.isPercent ? marginRightValue.value / 100 : -1,
            bottomMargin: marginBottomValue.isPercent ? 0 : marginBottomValue.value,
            bottomMarginPercent: marginBottomValue.isPercent ? marginBottomValue.value / 100 : -1,
            horizontalAlignment: style.horizontalAlignment,
            verticalAlignment: style.verticalAlignment
        };

    style._setValue(nativeLayoutParamsProperty, layoutParams);
}

function onPaddingValueChanged(data: PropertyChangeData) {
    var style = <Style>data.object;
    var thickness: definition.Thickness = {
        top: style.paddingTop,
        right: style.paddingRight,
        bottom: style.paddingBottom,
        left: style.paddingLeft
    };

    style._setValue(nativePaddingsProperty, thickness);
}

function thicknessComparer(x: definition.Thickness, y: definition.Thickness): boolean {
    if (x && y) {
        return x.left === y.left && x.top === y.top && x.right === y.right && x.bottom === y.bottom;
    }
    return !x === !y;
}

function convertToPercentHelper(value: Object): PercentHelper {
    let numberValue = 0;
    let isPercent = false;
    let isError = true;

    if (types.isString(value)) {
        var stringValue = (<string>value).trim();
        var percentIndex = stringValue.indexOf("%");
        if (percentIndex !== -1) {
            // if only % or % is not last we treat it as invalid value.
            if (percentIndex !== (stringValue.length - 1) || percentIndex === 0) {
                numberValue = 0;
            }
            else {
                isPercent = true;
                numberValue = converters.numberConverter(stringValue.substring(0, stringValue.length - 1).trim());
                isError = numberValue === 0;
            }
        }
        else {
            isError = false;
            isPercent = false;
            numberValue = converters.numberConverter(stringValue);
        }
    }
    else if (types.isNumber(value)) {
        isError = false;
        isPercent = false;
        numberValue = <number>value;
    }

    return {
        isError: isError,
        isPercent: isPercent,
        value: numberValue
    }
}

function numberOrPercentConverter(value: Object) {
    let result = convertToPercentHelper(value);
    if (result.isError) {
        throw new Error("Invalid value: " + value);
    }
    return result.isPercent ? value : result.value;
}

function isWidthHeightValid(value: Object): boolean {
    var result = convertToPercentHelper(value);
    if (result.isError) {
        return false;
    }

    return isNaN(result.value) || (result.value >= 0.0 && isFinite(result.value));
}

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

function onBackgroundColorPropertyChanged(data: PropertyChangeData) {
    var style = <Style>data.object;
    var currentBackground = <background.Background>style._getValue(backgroundInternalProperty);
    if (!Color.equals(currentBackground.color, data.newValue)) {
        style._setValue(backgroundInternalProperty, currentBackground.withColor(data.newValue));
    }
}

function onBackgroundImagePropertyChanged(data: PropertyChangeData) {
    var style = <Style>data.object;
    var url: string = data.newValue;
    var currentBackground = <background.Background>style._getValue(backgroundInternalProperty);
    var isValid = false;

    if (types.isString(data.newValue)) {
        var pattern: RegExp = /url\(('|")(.*?)\1\)/;
        var match = url.match(pattern);
        if (match && match[2]) {
            url = match[2];
        }

        ensureImageSource();

        if (utils.isDataURI(url)) {
            var base64Data = url.split(",")[1];
            if (types.isDefined(base64Data)) {
                style._setValue(backgroundInternalProperty, currentBackground.withImage(imageSource.fromBase64(base64Data)));
                isValid = true;
            }
        }
        else if (utils.isFileOrResourcePath(url)) {
            style._setValue(backgroundInternalProperty, currentBackground.withImage(imageSource.fromFileOrResource(url)));
            isValid = true;
        }
        else if (url.indexOf("http") !== -1) {
            style["_url"] = url;
            style._setValue(backgroundInternalProperty, currentBackground.withImage(undefined));
            imageSource.fromUrl(url).then((r) => {
                if (style && style["_url"] === url) {
                    // Get the current background again, as it might have changed while doing the request.
                    currentBackground = <background.Background>style._getValue(backgroundInternalProperty);
                    style._setValue(backgroundInternalProperty, currentBackground.withImage(r));
                }
            });
            isValid = true;
        }
    }

    if (!isValid) {
        style._setValue(backgroundInternalProperty, currentBackground.withImage(undefined));
    }
}

function onBackgroundRepeatPropertyChanged(data: PropertyChangeData) {
    var style = <Style>data.object;
    var currentBackground = <background.Background>style._getValue(backgroundInternalProperty);
    if (data.newValue !== currentBackground.repeat) {
        style._setValue(backgroundInternalProperty, currentBackground.withRepeat(data.newValue));
    }
}

function onBackgroundPositionPropertyChanged(data: PropertyChangeData) {
    var style = <Style>data.object;
    var currentBackground = <background.Background>style._getValue(backgroundInternalProperty);
    if (data.newValue !== currentBackground.position) {
        style._setValue(backgroundInternalProperty, currentBackground.withPosition(data.newValue));
    }
}

function onBackgroundSizePropertyChanged(data: PropertyChangeData) {
    var style = <Style>data.object;
    var currentBackground = <background.Background>style._getValue(backgroundInternalProperty);
    if (data.newValue !== currentBackground.size) {
        style._setValue(backgroundInternalProperty, currentBackground.withSize(data.newValue));
    }
}

function onBorderWidthPropertyChanged(data: PropertyChangeData) {
    if (platform.isAndroid) {
        var style = <Style>data.object;
        var currentBackground = <background.Background>style._getValue(backgroundInternalProperty);
        if (data.newValue !== currentBackground.borderWidth) {
            style._setValue(backgroundInternalProperty, currentBackground.withBorderWidth(data.newValue));
        }
    }
}

function onBorderColorPropertyChanged(data: PropertyChangeData) {
    if (platform.isAndroid) {
        var style = <Style>data.object;
        var currentBackground = <background.Background>style._getValue(backgroundInternalProperty);
        if (data.newValue !== currentBackground.borderColor) {
            style._setValue(backgroundInternalProperty, currentBackground.withBorderColor(data.newValue));
        }
    }
}

function onBorderRadiusPropertyChanged(data: PropertyChangeData) {
    if (platform.isAndroid) {
        var style = <Style>data.object;
        var currentBackground = <background.Background>style._getValue(backgroundInternalProperty);
        if (data.newValue !== currentBackground.borderRadius) {
            style._setValue(backgroundInternalProperty, currentBackground.withBorderRadius(data.newValue));
        }
    }
}

function onClipPathPropertyChanged(data: PropertyChangeData) {
    if (platform.isAndroid) {
        var style = <Style>data.object;
        var currentBackground = <background.Background>style._getValue(backgroundInternalProperty);
        if (data.newValue !== currentBackground.clipPath) {
            style._setValue(backgroundInternalProperty, currentBackground.withClipPath(data.newValue));
        }
    }
}

function getHandlerInternal(propertyId: number, classInfo: types.ClassInfo): definition.StylePropertyChangedHandler {
    var className = classInfo ? classInfo.name : "default";
    var handlerKey = className + propertyId;

    // try the cache first
    var result = _handlersCache[handlerKey];
    if (types.isDefined(result)) {
        return result;
    }

    var propertyHandlers = _registeredHandlers[propertyId];
    if (noStylingClasses.hasOwnProperty(className) || !propertyHandlers) {
        // Reached 'no-styling' class or no property handlers are registered for this propertyID
        result = null;
    }
    else if (propertyHandlers.hasOwnProperty(className)) {
        // Found handler for this class
        result = propertyHandlers[className];
    }
    else if (classInfo) {
        // Check the base class
        result = getHandlerInternal(propertyId, classInfo.baseClassInfo);
    }
    else {
        result = null;
    }

    _handlersCache[handlerKey] = result;
    return result;
}

function isVisibilityValid(value: string): boolean {
    return value === enums.Visibility.visible || value === enums.Visibility.collapse || value === enums.Visibility.collapsed;
}

function isTextDecorationValid(value: string): boolean {
    var values = (value + "").split(" ");

    return values.indexOf(enums.TextDecoration.none) !== -1 || values.indexOf(enums.TextDecoration.underline) !== -1 || values.indexOf(enums.TextDecoration.lineThrough) !== -1;
}

function isTextTransformValid(value: string): boolean {
    return value === enums.TextTransform.none || value === enums.TextTransform.uppercase || value === enums.TextTransform.lowercase || value === enums.TextTransform.capitalize;
}

function isWhiteSpaceValid(value: string): boolean {
    return value === enums.WhiteSpace.nowrap || value === enums.WhiteSpace.normal;
}

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

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

function isMarginValid(value: number): boolean {
    var result = convertToPercentHelper(value);
    if (result.isError) {
        return false;
    }

    return isFinite(result.value) && !isNaN(result.value);
}

function isOpacityValid(value: string): boolean {
    var parsedValue: number = parseFloat(value);
    return !isNaN(parsedValue) && 0 <= parsedValue && parsedValue <= 1;
}

function isFloatValueValid(value: string): boolean {
    var parsedValue: number = parseFloat(value);
    return !isNaN(parsedValue);
}

function isFontWeightValid(value: string): boolean {
    if (!value) {
        console.trace();
    }
    return value === enums.FontWeight.thin
        || value === enums.FontWeight.extraLight
        || value === enums.FontWeight.light
        || value === enums.FontWeight.normal || value === "400"
        || value === enums.FontWeight.medium
        || value === enums.FontWeight.semiBold
        || value === enums.FontWeight.bold || value === "700"
        || value === enums.FontWeight.extraBold
        || value === enums.FontWeight.black
        ;
}

function isFontStyleValid(value: string): boolean {
    return value === enums.FontStyle.normal || value === enums.FontStyle.italic;
}

function onVisibilityChanged(data: PropertyChangeData) {
    (<any>data.object)._view._isVisibleCache = data.newValue === enums.Visibility.visible;
}

function onFontFamilyChanged(data: PropertyChangeData) {
    var style = <Style>data.object;

    var currentFont = <font.Font>style._getValue(fontInternalProperty);
    if (currentFont.fontFamily !== data.newValue) {
        style._setValue(fontInternalProperty, currentFont.withFontFamily(data.newValue));
    }
}

function onFontStyleChanged(data: PropertyChangeData) {
    var style = <Style>data.object;

    var currentFont = <font.Font>style._getValue(fontInternalProperty);
    if (currentFont.fontStyle !== data.newValue) {
        style._setValue(fontInternalProperty, currentFont.withFontStyle(data.newValue));
    }
}

function onFontWeightChanged(data: PropertyChangeData) {
    var style = <Style>data.object;

    var currentFont = <font.Font>style._getValue(fontInternalProperty);
    if (currentFont.fontWeight !== data.newValue) {
        style._setValue(fontInternalProperty, currentFont.withFontWeight(data.newValue));
    }
}

function onFontSizeChanged(data: PropertyChangeData) {
    var style = <Style>data.object;

    var currentFont = <font.Font>style._getValue(fontInternalProperty);
    if (currentFont.fontSize !== data.newValue) {
        style._setValue(fontInternalProperty, currentFont.withFontSize(data.newValue));
    }
}

export class Style extends DependencyObservable implements styling.Style {
    private _view: View;
    private _updateCounter = 0;
    private _nativeSetters = new Map<Property, any>();

    get rotate(): number {
        return this._getValue(rotateProperty);
    }

    set rotate(value: number) {
        this._setValue(rotateProperty, value);
    }

    get scaleX(): number {
        return this._getValue(scaleXProperty);
    }

    set scaleX(value: number) {
        this._setValue(scaleXProperty, value);
    }

    get scaleY(): number {
        return this._getValue(scaleYProperty);
    }

    set scaleY(value: number) {
        this._setValue(scaleYProperty, value);
    }

    get translateX(): number {
        return this._getValue(translateXProperty);
    }

    set translateX(value: number) {
        this._setValue(translateXProperty, value);
    }

    get translateY(): number {
        return this._getValue(translateYProperty);
    }

    set translateY(value: number) {
        this._setValue(translateYProperty, value);
    }

    get color(): Color {
        return this._getValue(colorProperty);
    }
    set color(value: Color) {
        this._setValue(colorProperty, value);
    }

    get placeholderColor(): Color {
        return this._getValue(placeholderColorProperty);
    }
    set placeholderColor(value: Color) {
        this._setValue(placeholderColorProperty, value);
    }

    get backgroundColor(): Color {
        return this._getValue(backgroundColorProperty);
    }
    set backgroundColor(value: Color) {
        this._setValue(backgroundColorProperty, value);
    }

    get backgroundImage(): string {
        return this._getValue(backgroundImageProperty);
    }
    set backgroundImage(value: string) {
        this._setValue(backgroundImageProperty, value);
    }

    get backgroundRepeat(): string {
        return this._getValue(backgroundRepeatProperty);
    }
    set backgroundRepeat(value: string) {
        this._setValue(backgroundRepeatProperty, value);
    }

    get backgroundSize(): string {
        return this._getValue(backgroundSizeProperty);
    }
    set backgroundSize(value: string) {
        this._setValue(backgroundSizeProperty, value);
    }

    get backgroundPosition(): string {
        return this._getValue(backgroundPositionProperty);
    }
    set backgroundPosition(value: string) {
        this._setValue(backgroundPositionProperty, value);
    }

    get borderColor(): Color {
        return this._getValue(borderColorProperty);
    }
    set borderColor(value: Color) {
        this._setValue(borderColorProperty, value);
    }

    get borderWidth(): number {
        return this._getValue(borderWidthProperty);
    }
    set borderWidth(value: number) {
        this._setValue(borderWidthProperty, value);
    }

    get borderRadius(): number {
        return this._getValue(borderRadiusProperty);
    }
    set borderRadius(value: number) {
        this._setValue(borderRadiusProperty, value);
    }

    get clipPath(): string {
        return this._getValue(clipPathProperty);
    }
    set clipPath(value: string) {
        this._setValue(clipPathProperty, value);
    }

    get fontSize(): number {
        return this._getValue(fontSizeProperty);
    }
    set fontSize(value: number) {
        this._setValue(fontSizeProperty, value);
    }

    get fontFamily(): string {
        return this._getValue(fontFamilyProperty);
    }
    set fontFamily(value: string) {
        this._setValue(fontFamilyProperty, value);
    }

    get fontStyle(): string {
        return this._getValue(fontStyleProperty);
    }
    set fontStyle(value: string) {
        this._setValue(fontStyleProperty, value);
    }

    get fontWeight(): string {
        return this._getValue(fontWeightProperty);
    }
    set fontWeight(value: string) {
        this._setValue(fontWeightProperty, value);
    }

    get font(): string {
        return this.fontStyle + " " + this.fontWeight + " " + this.fontSize + " " + this.fontFamily;
    }
    set font(value: string) {
        this._setShorthandProperty("font", value);
    }

    get _fontInternal(): font.Font {
        return this._getValue(fontInternalProperty);
    }
    set _fontInternal(value: font.Font) {
        this._setValue(fontInternalProperty, value);
    }

    get textAlignment(): string {
        return this._getValue(textAlignmentProperty);
    }
    set textAlignment(value: string) {
        this._setValue(textAlignmentProperty, value);
    }

    get minWidth(): number {
        return this._getValue(minWidthProperty);
    }
    set minWidth(value: number) {
        this._setValue(minWidthProperty, value);
    }

    get minHeight(): number {
        return this._getValue(minHeightProperty);
    }
    set minHeight(value: number) {
        this._setValue(minHeightProperty, value);
    }

    get width(): number {
        return this._getValue(widthProperty);
    }
    set width(value: number) {
        this._setValue(widthProperty, value);
    }

    get height(): number {
        return this._getValue(heightProperty);
    }
    set height(value: number) {
        this._setValue(heightProperty, value);
    }

    get margin(): string {
        return this.marginTop + " " + this.marginRight + " " + this.marginBottom + " " + this.marginLeft;
    }
    set margin(value: string) {
        this._setShorthandProperty("margin", value);
    }

    get marginLeft(): number {
        return this._getValue(marginLeftProperty);
    }
    set marginLeft(value: number) {
        this._setValue(marginLeftProperty, value);
    }

    get marginTop(): number {
        return this._getValue(marginTopProperty);
    }
    set marginTop(value: number) {
        this._setValue(marginTopProperty, value);
    }

    get marginRight(): number {
        return this._getValue(marginRightProperty);
    }
    set marginRight(value: number) {
        this._setValue(marginRightProperty, value);
    }

    get marginBottom(): number {
        return this._getValue(marginBottomProperty);
    }
    set marginBottom(value: number) {
        this._setValue(marginBottomProperty, value);
    }

    get padding(): string {
        return this.paddingTop + " " + this.paddingRight + " " + this.paddingBottom + " " + this.paddingLeft;
    }
    set padding(value: string) {
        this._setShorthandProperty("padding", value);
    }

    get paddingLeft(): number {
        return this._getValue(paddingLeftProperty);
    }
    set paddingLeft(value: number) {
        this._setValue(paddingLeftProperty, value);
    }

    get paddingTop(): number {
        return this._getValue(paddingTopProperty);
    }
    set paddingTop(value: number) {
        this._setValue(paddingTopProperty, value);
    }

    get paddingRight(): number {
        return this._getValue(paddingRightProperty);
    }
    set paddingRight(value: number) {
        this._setValue(paddingRightProperty, value);
    }

    get paddingBottom(): number {
        return this._getValue(paddingBottomProperty);
    }
    set paddingBottom(value: number) {
        this._setValue(paddingBottomProperty, value);
    }

    get horizontalAlignment(): string {
        return this._getValue(horizontalAlignmentProperty);
    }
    set horizontalAlignment(value: string) {
        this._setValue(horizontalAlignmentProperty, value);
    }

    get verticalAlignment(): string {
        return this._getValue(verticalAlignmentProperty);
    }
    set verticalAlignment(value: string) {
        this._setValue(verticalAlignmentProperty, value);
    }

    get visibility(): string {
        return this._getValue(visibilityProperty);
    }
    set visibility(value: string) {
        this._setValue(visibilityProperty, value);
    }

    get opacity(): number {
        return this._getValue(opacityProperty);
    }
    set opacity(value: number) {
        this._setValue(opacityProperty, value);
    }

    get textDecoration(): string {
        return this._getValue(textDecorationProperty);
    }
    set textDecoration(value: string) {
        this._setValue(textDecorationProperty, value);
    }

    get textTransform(): string {
        return this._getValue(textTransformProperty);
    }
    set textTransform(value: string) {
        this._setValue(textTransformProperty, value);
    }

    get whiteSpace(): string {
        return this._getValue(whiteSpaceProperty);
    }
    set whiteSpace(value: string) {
        this._setValue(whiteSpaceProperty, value);
    }

    get letterSpacing(): number {
        return this._getValue(letterSpacingProperty);
    }
    set letterSpacing(value: number) {
        this._setValue(letterSpacingProperty, value);
    }

    get zIndex(): number {
        return this._getValue(zIndexProperty);
    }
    set zIndex(value: number) {
        this._setValue(zIndexProperty, value);
    }

    public _updateTextDecoration() {
        if (this._getValue(textDecorationProperty) !== enums.TextDecoration.none) {
            this._applyProperty(textDecorationProperty, this._getValue(textDecorationProperty));
        }
    }

    public _updateTextTransform() {
        if (this._getValue(textTransformProperty) !== enums.TextTransform.none) {
            this._applyProperty(textTransformProperty, this._getValue(textTransformProperty));
        }
    }

    constructor(parentView: View) {
        super();
        this._view = parentView;
    }

    public _beginUpdate() {
        this._updateCounter++;
    }

    public _endUpdate() {
        this._updateCounter--;
        if (this._updateCounter < 0) {
            throw new Error("style._endUpdate() called, but no update is in progress.");
        }

        if (this._updateCounter === 0) {
            this._nativeSetters.forEach((newValue, property, map) => { this._applyStyleProperty(property, newValue); });
            this._nativeSetters.clear();
        }
    }

    public _resetCssValues() {
        this._view._unregisterAllAnimations();
        this._resetValues(ValueSource.Css);
    }

    public _resetLocalValues() {
        this._resetValues(ValueSource.Local);
    }

    public _inheritStyleProperties(parent: View) {
        parent.style._eachSetPropertyValue((property, value) => {
            if (property.inheritable) {
                // this._inheritStyleProperty(property, value);
                this._setValue(property, value, ValueSource.Inherited);
            }

            return true;
        });
    }

    public _onPropertyChanged(property: Property, oldValue: any, newValue: any) {
        if (trace.enabled) {
            trace.write(
                "Style._onPropertyChanged view:" + this._view +
                ", property: " + property.name +
                ", oldValue: " + oldValue +
                ", newValue: " + newValue, trace.categories.Style);
        }

        super._onPropertyChanged(property, oldValue, newValue);

        this._view._checkMetadataOnPropertyChanged(property.metadata);

        this._applyProperty(property, newValue);
    }

    public _sizeChanged() {
        if (!(<background.Background>this._getValue(backgroundInternalProperty)).isEmpty()) {
            this._applyStyleProperty(backgroundInternalProperty, this._getValue(backgroundInternalProperty));
        }

        var clipPathPropertyValue = this._getValue(clipPathProperty);
        if (types.isString(clipPathPropertyValue) && clipPathPropertyValue !== "") {
            this._applyStyleProperty(clipPathProperty, clipPathPropertyValue);
        }
    }

    public _syncNativeProperties() {
        this._eachSetPropertyValue((property, value) => {
            this._applyStyleProperty(property, value);
            return true;
        });
    }

    private _applyProperty(property: Property, newValue: any) {
        this._applyStyleProperty(property, newValue);

        // The effective value of an inheritable property has changed
        // propagate the change down to the descendants to update their inherited properties.
        if (property.inheritable && this._view._childrenCount > 0) {
            this._view._eachChildView((child: View) => {
                // child.style._inheritStyleProperty(property);
                child.style._setValue(property, newValue, ValueSource.Inherited);
                return true;
            });
        }
    }

    private _applyStyleProperty(property: Property, newValue: any) {
        if (!this._view._shouldApplyStyleHandlers()) {
            return;
        }

        if (this._updateCounter > 0) {
            this._nativeSetters.set(property, newValue);
            return;
        }

        let handler: definition.StylePropertyChangedHandler = getHandler(property, this._view);
        if (!handler) {
            if (trace.enabled) {
                trace.write("No handler for property: " + property.name + " with id: " + property.id + ", view:" + this._view, trace.categories.Style);
            }
        }
        else {
            if (trace.enabled) {
                trace.write("Found handler for property: " + property.name + ", view:" + this._view, trace.categories.Style);
            }

            let shouldReset = false;
            if (property.equalityComparer) {
                shouldReset = property.equalityComparer(newValue, property.defaultValue);
            }
            else {
                shouldReset = (newValue === property.defaultValue);
            }

            if (shouldReset) {
                (<any>handler).resetProperty(property, this._view);
            } else {
                (<any>handler).applyProperty(property, this._view, newValue);
            }

            this._view._onStylePropertyChanged(property);
        }
    }

    get _nativeView(): any {
        return this._view._nativeView;
    }

    private _setShorthandProperty(name: string, value: any): void {
        var pairs = styleProperty.getShorthandPairs(name, value);
        if (pairs) {
            this._beginUpdate();
            for (let j = 0; j < pairs.length; j++) {
                let pair = pairs[j];
                this._setValue(pair.property, pair.value, ValueSource.Local);
            }
            this._endUpdate();
        }
    }
}

export function registerHandler(property: Property, handler: definition.StylePropertyChangedHandler, className?: string) {
    var realClassName = className ? className : "default";

    var handlerRecord = _registeredHandlers[property.id];
    if (!handlerRecord) {
        handlerRecord = {};
        _registeredHandlers[property.id] = handlerRecord;
    }

    handlerRecord[realClassName] = handler;
}

export function registerNoStylingClass(className) {
    noStylingClasses[className] = 1;
}

export function getHandler(property: Property, view: View): definition.StylePropertyChangedHandler {
    return getHandlerInternal(property.id, types.getClassInfo(view));
}

// Property registration

export var rotateProperty = new styleProperty.Property("rotate", "rotate",
    new PropertyMetadata(undefined, PropertyMetadataSettings.None, null));

export var scaleXProperty = new styleProperty.Property("scaleX", "scaleX",
    new PropertyMetadata(undefined, PropertyMetadataSettings.None, null));

export var scaleYProperty = new styleProperty.Property("scaleY", "scaleY",
    new PropertyMetadata(undefined, PropertyMetadataSettings.None, null));

export var translateXProperty = new styleProperty.Property("translateX", "translateX",
    new PropertyMetadata(undefined, PropertyMetadataSettings.None, null));

export var translateYProperty = new styleProperty.Property("translateY", "translateY",
    new PropertyMetadata(undefined, PropertyMetadataSettings.None, null));

export var colorProperty = new styleProperty.Property("color", "color",
    new PropertyMetadata(undefined, PropertyMetadataSettings.Inheritable, undefined, Color.isValid, Color.equals),
    converters.colorConverter);

export var placeholderColorProperty = new styleProperty.Property("placeholderColor", "placeholder-color",
    new PropertyMetadata(undefined, PropertyMetadataSettings.None, undefined, Color.isValid, Color.equals),
    converters.colorConverter);

export var backgroundImageProperty = new styleProperty.Property("backgroundImage", "background-image",
    new PropertyMetadata(undefined, PropertyMetadataSettings.None, onBackgroundImagePropertyChanged));

export var backgroundColorProperty = new styleProperty.Property("backgroundColor", "background-color",
    new PropertyMetadata(undefined, PropertyMetadataSettings.None, onBackgroundColorPropertyChanged, Color.isValid, Color.equals), converters.colorConverter);

export var backgroundRepeatProperty = new styleProperty.Property("backgroundRepeat", "background-repeat",
    new PropertyMetadata(undefined, PropertyMetadataSettings.None, onBackgroundRepeatPropertyChanged));

export var backgroundSizeProperty = new styleProperty.Property("backgroundSize", "background-size",
    new PropertyMetadata(undefined, PropertyMetadataSettings.None, onBackgroundSizePropertyChanged));

export var backgroundPositionProperty = new styleProperty.Property("backgroundPosition", "background-position",
    new PropertyMetadata(undefined, PropertyMetadataSettings.None, onBackgroundPositionPropertyChanged));

export var borderWidthProperty = new styleProperty.Property("borderWidth", "border-width",
    new PropertyMetadata(0, AffectsLayout, onBorderWidthPropertyChanged, isNonNegativeFiniteNumber), converters.numberConverter);

export var borderColorProperty = new styleProperty.Property("borderColor", "border-color",
    new PropertyMetadata(undefined, PropertyMetadataSettings.None, onBorderColorPropertyChanged, Color.isValid, Color.equals), converters.colorConverter);

export var borderRadiusProperty = new styleProperty.Property("borderRadius", "border-radius",
    new PropertyMetadata(0, AffectsLayout, onBorderRadiusPropertyChanged, isNonNegativeFiniteNumber), converters.numberConverter);

export var clipPathProperty = new styleProperty.Property("clipPath", "clip-path",
    new PropertyMetadata(undefined, AffectsLayout, onClipPathPropertyChanged, isClipPathValid));

export var backgroundInternalProperty = new styleProperty.Property("_backgroundInternal", "_backgroundInternal",
    new PropertyMetadata(background.Background.default, PropertyMetadataSettings.None, undefined, undefined, background.Background.equals));

export var fontSizeProperty = new styleProperty.Property("fontSize", "font-size",
    new PropertyMetadata(undefined, PropertyMetadataSettings.Inheritable, onFontSizeChanged), converters.fontSizeConverter);

export var fontFamilyProperty = new styleProperty.Property("fontFamily", "font-family",
    new PropertyMetadata(undefined, PropertyMetadataSettings.Inheritable, onFontFamilyChanged));

export var fontStyleProperty = new styleProperty.Property("fontStyle", "font-style",
    new PropertyMetadata(enums.FontStyle.normal, PropertyMetadataSettings.Inheritable, onFontStyleChanged, isFontStyleValid));

export var fontWeightProperty = new styleProperty.Property("fontWeight", "font-weight",
    new PropertyMetadata(enums.FontWeight.normal, PropertyMetadataSettings.Inheritable, onFontWeightChanged, isFontWeightValid));

export var fontInternalProperty = new styleProperty.Property("_fontInternal", "_fontInternal",
    new PropertyMetadata(font.Font.default, AffectsLayout, null, null, font.Font.equals), font.Font.parse);

export var textAlignmentProperty = new styleProperty.Property("textAlignment", "text-align",
    new PropertyMetadata(undefined, AffectsLayout | PropertyMetadataSettings.Inheritable), converters.textAlignConverter);

export var minWidthProperty = new styleProperty.Property("minWidth", "min-width",
    new PropertyMetadata(0, AffectsLayout, null, isMinWidthHeightValid), converters.numberConverter);

export var minHeightProperty = new styleProperty.Property("minHeight", "min-height",
    new PropertyMetadata(0, AffectsLayout, null, isMinWidthHeightValid), converters.numberConverter);

export var visibilityProperty = new styleProperty.Property("visibility", "visibility",
    new PropertyMetadata(enums.Visibility.visible, AffectsLayout, onVisibilityChanged, isVisibilityValid), converters.visibilityConverter);

export var opacityProperty = new styleProperty.Property("opacity", "opacity",
    new PropertyMetadata(1.0, PropertyMetadataSettings.None, undefined, isOpacityValid), converters.opacityConverter);

export var textDecorationProperty = new styleProperty.Property("textDecoration", "text-decoration",
    new PropertyMetadata(enums.TextDecoration.none, PropertyMetadataSettings.None, undefined, isTextDecorationValid), converters.textDecorationConverter);

export var textTransformProperty = new styleProperty.Property("textTransform", "text-transform",
    new PropertyMetadata(enums.TextTransform.none, PropertyMetadataSettings.None, undefined, isTextTransformValid), converters.textTransformConverter);

export var whiteSpaceProperty = new styleProperty.Property("whiteSpace", "white-space",
    new PropertyMetadata(undefined, AffectsLayout, undefined, isWhiteSpaceValid), converters.whiteSpaceConverter);

export var letterSpacingProperty = new styleProperty.Property("letterSpacing", "letter-spacing",
    new PropertyMetadata(Number.NaN, AffectsLayout, undefined, isFloatValueValid), converters.floatConverter);

export var zIndexProperty = new styleProperty.Property("zIndex", "z-index",
    new PropertyMetadata(Number.NaN, AffectsLayout, undefined, isFloatValueValid), converters.floatConverter);

// Helper property holding most layout related properties available in CSS.
// When layout related properties are set in CSS we chache them and send them to the native view in a single call.
export var nativeLayoutParamsProperty = new styleProperty.Property("nativeLayoutParams", "nativeLayoutParams",
    new PropertyMetadata({
        width: -1,
        widthPercent: -1,
        height: -1,
        heightPercent: -1,
        leftMargin: 0,
        leftMarginPercent: -1,
        topMargin: 0,
        topMarginPercent: -1,
        rightMargin: 0,
        rightMarginPercent: -1,
        bottomMargin: 0,
        bottomMarginPercent: -1,
        horizontalAlignment: enums.HorizontalAlignment.stretch,
        verticalAlignment: enums.VerticalAlignment.stretch
    }, null, null, null, layoutParamsComparer));

export var widthProperty = new styleProperty.Property("width", "width",
    new PropertyMetadata(Number.NaN, AffectsLayout, onLayoutParamsChanged, isWidthHeightValid), numberOrPercentConverter);

export var heightProperty = new styleProperty.Property("height", "height",
    new PropertyMetadata(Number.NaN, AffectsLayout, onLayoutParamsChanged, isWidthHeightValid), numberOrPercentConverter);

export var marginLeftProperty = new styleProperty.Property("marginLeft", "margin-left",
    new PropertyMetadata(0, AffectsLayout, onLayoutParamsChanged, isMarginValid), numberOrPercentConverter);

export var marginRightProperty = new styleProperty.Property("marginRight", "margin-right",
    new PropertyMetadata(0, AffectsLayout, onLayoutParamsChanged, isMarginValid), numberOrPercentConverter);

export var marginTopProperty = new styleProperty.Property("marginTop", "margin-top",
    new PropertyMetadata(0, AffectsLayout, onLayoutParamsChanged, isMarginValid), numberOrPercentConverter);

export var marginBottomProperty = new styleProperty.Property("marginBottom", "margin-bottom",
    new PropertyMetadata(0, AffectsLayout, onLayoutParamsChanged, isMarginValid), numberOrPercentConverter);

export var verticalAlignmentProperty = new styleProperty.Property("verticalAlignment", "vertical-align",
    new PropertyMetadata(enums.VerticalAlignment.stretch, AffectsLayout, onLayoutParamsChanged));

export var horizontalAlignmentProperty = new styleProperty.Property("horizontalAlignment", "horizontal-align",
    new PropertyMetadata(enums.HorizontalAlignment.stretch, AffectsLayout, onLayoutParamsChanged));

function getNativePadding(nativeView: android.view.View, callback: (view: android.view.View) => number): NativeValueResult {
    return {
        result: nativeView ? callback(nativeView) / utils.layout.getDisplayDensity() : 0,
        cacheable: !!nativeView
    };
}

function getNativePaddingLeft(instance: DependencyObservable): NativeValueResult {
    var nativeView: android.view.View = (<any>instance)._nativeView;
    return getNativePadding(nativeView, (view) => { return view.getPaddingLeft(); });
}

function getNativePaddingTop(instance: DependencyObservable): NativeValueResult {
    var nativeView: android.view.View = (<any>instance)._nativeView;
    return getNativePadding(nativeView, (view) => { return view.getPaddingTop(); });
}

function getNativePaddingRight(instance: DependencyObservable): NativeValueResult {
    var nativeView: android.view.View = (<any>instance)._nativeView;
    return getNativePadding(nativeView, (view) => { return view.getPaddingRight(); });
}

function getNativePaddingBottom(instance: DependencyObservable): NativeValueResult {
    var nativeView: android.view.View = (<any>instance)._nativeView;
    return getNativePadding(nativeView, (view) => { return view.getPaddingBottom(); });
}

// Helper property holding all paddings. When paddings are set through CSS we cache them and send them to the native view in a single call.
export var nativePaddingsProperty = new styleProperty.Property("paddingNative", "paddingNative",
    new PropertyMetadata(undefined, null, null, null, thicknessComparer));

// TODO: separate into .android/.ios files so that there is no need for such checks
var defaultPadding = platform.isAndroid ? undefined : 0;

export var paddingLeftProperty = new styleProperty.Property("paddingLeft", "padding-left",
    new PropertyMetadata(defaultPadding, AffectsLayout, onPaddingValueChanged, isNonNegativeFiniteNumber), converters.numberConverter);

export var paddingRightProperty = new styleProperty.Property("paddingRight", "padding-right",
    new PropertyMetadata(defaultPadding, AffectsLayout, onPaddingValueChanged, isNonNegativeFiniteNumber), converters.numberConverter);

export var paddingTopProperty = new styleProperty.Property("paddingTop", "padding-top",
    new PropertyMetadata(defaultPadding, AffectsLayout, onPaddingValueChanged, isNonNegativeFiniteNumber), converters.numberConverter);

export var paddingBottomProperty = new styleProperty.Property("paddingBottom", "padding-bottom",
    new PropertyMetadata(defaultPadding, AffectsLayout, onPaddingValueChanged, isNonNegativeFiniteNumber), converters.numberConverter);

// TODO: separate into .android/.ios files so that there is no need for such checks
if (platform.device.os === platform.platformNames.android) {
    paddingTopProperty.defaultValueGetter = getNativePaddingTop;
    paddingLeftProperty.defaultValueGetter = getNativePaddingLeft;
    paddingRightProperty.defaultValueGetter = getNativePaddingRight;
    paddingBottomProperty.defaultValueGetter = getNativePaddingBottom;
}

function onPaddingChanged(value: any): Array<styleProperty.KeyValuePair<styleProperty.Property, any>> {
    var thickness = parseThickness(value);
    var array = new Array<styleProperty.KeyValuePair<styleProperty.Property, any>>();
    array.push({ property: paddingTopProperty, value: thickness.top });
    array.push({ property: paddingRightProperty, value: thickness.right });
    array.push({ property: paddingBottomProperty, value: thickness.bottom });
    array.push({ property: paddingLeftProperty, value: thickness.left });
    return array;
}

function onMarginChanged(value: any): Array<styleProperty.KeyValuePair<styleProperty.Property, any>> {
    var thickness = parseMargin(value);
    var array = new Array<styleProperty.KeyValuePair<styleProperty.Property, any>>();
    array.push({ property: marginTopProperty, value: thickness.top });
    array.push({ property: marginRightProperty, value: thickness.right });
    array.push({ property: marginBottomProperty, value: thickness.bottom });
    array.push({ property: marginLeftProperty, value: thickness.left });
    return array;
}

function onFontChanged(value: any): Array<styleProperty.KeyValuePair<styleProperty.Property, any>> {
    var newFont = font.Font.parse(value);
    var array = new Array<styleProperty.KeyValuePair<styleProperty.Property, any>>();
    array.push({ property: fontFamilyProperty, value: newFont.fontFamily });
    array.push({ property: fontStyleProperty, value: newFont.fontStyle });
    array.push({ property: fontWeightProperty, value: newFont.fontWeight });
    array.push({ property: fontSizeProperty, value: newFont.fontSize });
    return array;
}

function onTransformChanged(value: any): Array<styleProperty.KeyValuePair<styleProperty.Property, any>> {
    var newTransform = converters.transformConverter(value);
    var array = new Array<styleProperty.KeyValuePair<styleProperty.Property, any>>();
    var values = undefined;
    for (var transform in newTransform) {
        switch (transform) {
            case "scaleX":
                array.push({ property: scaleXProperty, value: parseFloat(newTransform[transform]) });
                break;
            case "scaleY":
                array.push({ property: scaleYProperty, value: parseFloat(newTransform[transform]) });
                break;
            case "scale":
            case "scale3d":
                values = newTransform[transform].split(",");
                if (values.length === 2 || values.length === 3) {
                    array.push({ property: scaleXProperty, value: parseFloat(values[0]) });
                    array.push({ property: scaleYProperty, value: parseFloat(values[1]) });
                }
                break;
            case "translateX":
                array.push({ property: translateXProperty, value: parseFloat(newTransform[transform]) });
                break;
            case "translateY":
                array.push({ property: translateYProperty, value: parseFloat(newTransform[transform]) });
                break;
            case "translate":
            case "translate3d":
                values = newTransform[transform].split(",");
                if (values.length === 2 || values.length === 3) {
                    array.push({ property: translateXProperty, value: parseFloat(values[0]) });
                    array.push({ property: translateYProperty, value: parseFloat(values[1]) });
                }
                break;
            case "rotate":
                let text = newTransform[transform];
                let val = parseFloat(text);
                if (text.slice(-3) === "rad") {
                    val = val * (180.0 / Math.PI);
                }
                array.push({ property: rotateProperty, value: val });
                break;
            case "none":
                array.push({ property: scaleXProperty, value: 1 });
                array.push({ property: scaleYProperty, value: 1 });
                array.push({ property: translateXProperty, value: 0 });
                array.push({ property: translateYProperty, value: 0 });
                array.push({ property: rotateProperty, value: 0 });
                break;
        }
    }
    return array;
}

// register default shorthand callbacks.
styleProperty.registerShorthandCallback("font", onFontChanged);
styleProperty.registerShorthandCallback("margin", onMarginChanged);
styleProperty.registerShorthandCallback("padding", onPaddingChanged);
styleProperty.registerShorthandCallback("transform", onTransformChanged);

var _defaultNativeValuesCache = {};

export class StylePropertyChangedHandler {
    private _applyProperty: (view: View, newValue: any, defaultValue?: any) => void;
    private _resetProperty: (view: View, nativeValue: any) => void;
    private _getNativeValue: (view: View) => any;

    constructor(
        applyCallback: (view: View, newValue: any, defaultValue?: any) => void,
        resetCallback: (view: View, nativeValue: any) => void,
        getNativeValue?: (view: View) => any) {

        this._applyProperty = applyCallback;
        this._resetProperty = resetCallback;
        this._getNativeValue = getNativeValue;
    }

    public applyProperty(property: Property, view: View, newValue: any) {
        var className = types.getClass(view);
        if (!_defaultNativeValuesCache.hasOwnProperty(className + property.id) && this._getNativeValue) {
            _defaultNativeValuesCache[className + property.id] = this._getNativeValue(view);
        }

        if (application.android) {
            newValue = newValue.android ? newValue.android : newValue;
        } else if (application.ios) {
            newValue = newValue.ios ? newValue.ios : newValue;
        }

        this._applyProperty(view, newValue, _defaultNativeValuesCache[className + property.id]);
    }

    public resetProperty(property: Property, view: View) {
        var className = types.getClass(view);
        this._resetProperty(view, _defaultNativeValuesCache[className + property.id]);
    }
}

export var ignorePropertyHandler = new StylePropertyChangedHandler(
    (view, val) => {
        // empty
    },
    (view, val) => {
        // empty
    });

registerNoStylingClass("Frame");