/*
 * Copyright (C) 2013 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

WebInspector.CSSStyleManager = function()
{
    WebInspector.Object.call(this);

    if (window.CSSAgent)
        CSSAgent.enable();

    WebInspector.Frame.addEventListener(WebInspector.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this);
    WebInspector.Frame.addEventListener(WebInspector.Frame.Event.ResourceWasAdded, this._resourceAdded, this);
    WebInspector.Resource.addEventListener(WebInspector.SourceCode.Event.ContentDidChange, this._resourceContentDidChange, this);
    WebInspector.Resource.addEventListener(WebInspector.Resource.Event.TypeDidChange, this._resourceTypeDidChange, this);

    WebInspector.DOMNode.addEventListener(WebInspector.DOMNode.Event.AttributeModified, this._nodeAttributesDidChange, this);
    WebInspector.DOMNode.addEventListener(WebInspector.DOMNode.Event.AttributeRemoved, this._nodeAttributesDidChange, this);
    WebInspector.DOMNode.addEventListener(WebInspector.DOMNode.Event.EnabledPseudoClassesChanged, this._nodePseudoClassesDidChange, this);

    this._colorFormatSetting = new WebInspector.Setting("default-color-format", WebInspector.Color.Format.Original);

    this._styleSheetIdentifierMap = {};
    this._styleSheetFrameURLMap = {};
    this._nodeStylesMap = {};
};

WebInspector.CSSStyleManager.ForceablePseudoClasses = ["active", "focus", "hover", "visited"];

WebInspector.CSSStyleManager.prototype = {
    constructor: WebInspector.CSSStyleManager,

    // Public

    get preferredColorFormat()
    {
        return this._colorFormatSetting.value;
    },

    canForcePseudoClasses: function()
    {
        return window.CSSAgent && !!CSSAgent.forcePseudoState;
    },

    propertyNameHasOtherVendorPrefix: function(name)
    {
        if (!name || name.length < 4 || name.charAt(0) !== "-")
            return false;

        var match = name.match(/^(?:-moz-|-ms-|-o-|-epub-)/);
        if (!match)
            return false;

        return true;
    },

    propertyValueHasOtherVendorKeyword: function(value)
    {
        var match = value.match(/(?:-moz-|-ms-|-o-|-epub-)[-\w]+/);
        if (!match)
            return false;

        return true;
    },

    canonicalNameForPropertyName: function(name)
    {
        if (!name || name.length < 8 || name.charAt(0) !== "-")
            return name;

        var match = name.match(/^(?:-webkit-|-khtml-|-apple-)(.+)/);
        if (!match)
            return name;

        return match[1];
    },

    styleSheetForIdentifier: function(id)
    {
        if (id in this._styleSheetIdentifierMap)
            return this._styleSheetIdentifierMap[id];

        var styleSheet = new WebInspector.CSSStyleSheet(id);
        this._styleSheetIdentifierMap[id] = styleSheet;
        return styleSheet;
    },

    stylesForNode: function(node)
    {
        if (node.id in this._nodeStylesMap)
            return this._nodeStylesMap[node.id];

        var styles = new WebInspector.DOMNodeStyles(node);
        this._nodeStylesMap[node.id] = styles;
        return styles;
    },

    // Protected

    mediaQueryResultChanged: function()
    {
        // Called from WebInspector.CSSObserver.

        for (var key in this._nodeStylesMap)
            this._nodeStylesMap[key].mediaQueryResultDidChange();
    },

    styleSheetChanged: function(styleSheetIdentifier)
    {
        // Called from WebInspector.CSSObserver.

        var styleSheet = this.styleSheetForIdentifier(styleSheetIdentifier);
        console.assert(styleSheet);

        styleSheet.noteContentDidChange();

        this._updateResourceContent(styleSheet);
    },

    // Private

    _nodePseudoClassesDidChange: function(event)
    {
        var node = event.target;

        for (var key in this._nodeStylesMap) {
            var nodeStyles = this._nodeStylesMap[key];
            if (nodeStyles.node !== node && !nodeStyles.node.isDescendant(node))
                continue;
            nodeStyles.pseudoClassesDidChange(node);
        }
    },

    _nodeAttributesDidChange: function(event)
    {
        var node = event.target;

        for (var key in this._nodeStylesMap) {
            var nodeStyles = this._nodeStylesMap[key];
            if (nodeStyles.node !== node && !nodeStyles.node.isDescendant(node))
                continue;
            nodeStyles.attributeDidChange(node, event.data.name);
        }
    },

    _mainResourceDidChange: function(event)
    {
        console.assert(event.target instanceof WebInspector.Frame);

        if (!event.target.isMainFrame())
            return;

        // Clear our maps when the main frame navigates.

        this._styleSheetIdentifierMap = {};
        this._styleSheetFrameURLMap = {};
        this._nodeStylesMap = {};
    },

    _resourceAdded: function(event)
    {
        console.assert(event.target instanceof WebInspector.Frame);

        var resource = event.data.resource;
        console.assert(resource);

        if (resource.type !== WebInspector.Resource.Type.Stylesheet)
            return;

        this._clearStyleSheetsForResource(resource);
    },

    _resourceTypeDidChange: function(event)
    {
        console.assert(event.target instanceof WebInspector.Resource);

        var resource = event.target;
        if (resource.type !== WebInspector.Resource.Type.Stylesheet)
            return;

        this._clearStyleSheetsForResource(resource);
    },

    _clearStyleSheetsForResource: function(resource)
    {
        // Clear known stylesheets for this URL and frame. This will cause the stylesheets to
        // be updated next time _fetchInfoForAllStyleSheets is called.
        // COMPATIBILITY (iOS 6): The frame's id was not available for the key, so delete just the url too.
        delete this._styleSheetFrameURLMap[this._frameURLMapKey(resource.parentFrame, resource.url)];
        delete this._styleSheetFrameURLMap[resource.url];
    },

    _frameURLMapKey: function(frame, url)
    {
        return (frame ? frame.id + ":" : "") + url;
    },

    _lookupStyleSheetForResource: function(resource, callback)
    {
        this._lookupStyleSheet(resource.parentFrame, resource.url, callback);
    },

    _lookupStyleSheet: function(frame, url, callback)
    {
        console.assert(frame instanceof WebInspector.Frame);

        function syleSheetsFetched()
        {
            callback(this._styleSheetFrameURLMap[key] || this._styleSheetFrameURLMap[url] || null);
        }

        var key = this._frameURLMapKey(frame, url);

        // COMPATIBILITY (iOS 6): The frame's id was not available for the key, so check for just the url too.
        if (key in this._styleSheetFrameURLMap || url in this._styleSheetFrameURLMap)
            callback(this._styleSheetFrameURLMap[key] || this._styleSheetFrameURLMap[url] || null);
        else
            this._fetchInfoForAllStyleSheets(syleSheetsFetched.bind(this));
    },

    _fetchInfoForAllStyleSheets: function(callback)
    {
        console.assert(typeof callback === "function");

        function processStyleSheets(error, styleSheets)
        {
            this._styleSheetFrameURLMap = {};

            if (error) {
                callback();
                return;
            }

            for (var i = 0; i < styleSheets.length; ++i) {
                var styleSheetInfo = styleSheets[i];

                // COMPATIBILITY (iOS 6): The info did not have 'frameId', so make parentFrame null in that case.
                var parentFrame = "frameId" in styleSheetInfo ? WebInspector.frameResourceManager.frameForIdentifier(styleSheetInfo.frameId) : null;

                var styleSheet = this.styleSheetForIdentifier(styleSheetInfo.styleSheetId);
                styleSheet.updateInfo(styleSheetInfo.sourceURL, parentFrame);

                var key = this._frameURLMapKey(parentFrame, styleSheetInfo.sourceURL);
                this._styleSheetFrameURLMap[key] = styleSheet;
            }

            callback();
        }

        CSSAgent.getAllStyleSheets(processStyleSheets.bind(this));
    },

    _resourceContentDidChange: function(event)
    {
        var resource = event.target;
        if (resource === this._ignoreResourceContentDidChangeEventForResource)
            return;

        // Ignore if it isn't a CSS stylesheet.
        if (resource.type !== WebInspector.Resource.Type.Stylesheet || resource.syntheticMIMEType !== "text/css")
            return;

        function applyStyleSheetChanges()
        {
            function styleSheetFound(styleSheet)
            {
                delete resource.__pendingChangeTimeout;

                console.assert(styleSheet);
                if (!styleSheet)
                    return;

                // To prevent updating a TextEditor's content while the user is typing in it we want to
                // ignore the next _updateResourceContent call.
                resource.__ignoreNextUpdateResourceContent = true;

                WebInspector.branchManager.currentBranch.revisionForRepresentedObject(styleSheet).content = resource.content;
            }

            this._lookupStyleSheetForResource(resource, styleSheetFound.bind(this));
        }

        if (resource.__pendingChangeTimeout)
            clearTimeout(resource.__pendingChangeTimeout);
        resource.__pendingChangeTimeout = setTimeout(applyStyleSheetChanges.bind(this), 500);
    },

    _updateResourceContent: function(styleSheet)
    {
        console.assert(styleSheet);

        function fetchedStyleSheetContent(styleSheet, content)
        {
            delete styleSheet.__pendingChangeTimeout;

            console.assert(styleSheet.url);
            if (!styleSheet.url)
                return;

            var resource = null;

            // COMPATIBILITY (iOS 6): The stylesheet did not always have a frame, so fallback to looking
            // for the resource in all frames.
            if (styleSheet.parentFrame)
                resource = styleSheet.parentFrame.resourceForURL(styleSheet.url);
            else
                resource = WebInspector.frameResourceManager.resourceForURL(styleSheet.url);

            if (!resource)
                return;

            // Only try to update stylesheet resources. Other resources, like documents, can contain
            // multiple stylesheets and we don't have the source ranges to update those.
            if (resource.type !== WebInspector.Resource.Type.Stylesheet)
                return;

            if (resource.__ignoreNextUpdateResourceContent) {
                delete resource.__ignoreNextUpdateResourceContent;
                return;
            }

            this._ignoreResourceContentDidChangeEventForResource = resource;
            WebInspector.branchManager.currentBranch.revisionForRepresentedObject(resource).content = content;
            delete this._ignoreResourceContentDidChangeEventForResource;
        }

        function styleSheetReady()
        {
            styleSheet.requestContent(fetchedStyleSheetContent.bind(this));
        }

        function applyStyleSheetChanges()
        {
            if (styleSheet.url)
                styleSheetReady.call(this);
            else
                this._fetchInfoForAllStyleSheets(styleSheetReady.bind(this));
        }

        if (styleSheet.__pendingChangeTimeout)
            clearTimeout(styleSheet.__pendingChangeTimeout);
        styleSheet.__pendingChangeTimeout = setTimeout(applyStyleSheetChanges.bind(this), 500);
    }
};

WebInspector.CSSStyleManager.prototype.__proto__ = WebInspector.Object.prototype;
