/**
 * @license
 * Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
 * Use of this source code is governed by a BSD-style
 * license that can be found in the LICENSE file or at
 * https://developers.google.com/open-source/licenses/bsd
 */

/*
  This is a limited shim for ShadowDOM css styling.
  https://dvcs.w3.org/hg/webcomponents/raw-file/tip/spec/shadow/index.html#styles

  The intention here is to support only the styling features which can be
  relatively simply implemented. The goal is to allow users to avoid the
  most obvious pitfalls and do so without compromising performance significantly.
  For ShadowDOM styling that's not covered here, a set of best practices
  can be provided that should allow users to accomplish more complex styling.

  The following is a list of specific ShadowDOM styling features and a brief
  discussion of the approach used to shim.

  Shimmed features:

  * :host, :host-context: ShadowDOM allows styling of the shadowRoot's host
  element using the :host rule. To shim this feature, the :host styles are
  reformatted and prefixed with a given scope name and promoted to a
  document level stylesheet.
  For example, given a scope name of .foo, a rule like this:

    :host {
        background: red;
      }
    }

  becomes:

    .foo {
      background: red;
    }

  * encapsultion: Styles defined within ShadowDOM, apply only to
  dom inside the ShadowDOM. Polymer uses one of two techniques to imlement
  this feature.

  By default, rules are prefixed with the host element tag name
  as a descendant selector. This ensures styling does not leak out of the 'top'
  of the element's ShadowDOM. For example,

  div {
      font-weight: bold;
    }

  becomes:

  x-foo div {
      font-weight: bold;
    }

  becomes:


  Alternatively, if WebComponents.ShadowCSS.strictStyling is set to true then
  selectors are scoped by adding an attribute selector suffix to each
  simple selector that contains the host element tag name. Each element
  in the element's ShadowDOM template is also given the scope attribute.
  Thus, these rules match only elements that have the scope attribute.
  For example, given a scope name of x-foo, a rule like this:

    div {
      font-weight: bold;
    }

  becomes:

    div[x-foo] {
      font-weight: bold;
    }

  Note that elements that are dynamically added to a scope must have the scope
  selector added to them manually.

  * upper/lower bound encapsulation: Styles which are defined outside a
  shadowRoot should not cross the ShadowDOM boundary and should not apply
  inside a shadowRoot.

  This styling behavior is not emulated. Some possible ways to do this that
  were rejected due to complexity and/or performance concerns include: (1) reset
  every possible property for every possible selector for a given scope name;
  (2) re-implement css in javascript.

  As an alternative, users should make sure to use selectors
  specific to the scope in which they are working.

  * ::distributed: This behavior is not emulated. It's often not necessary
  to style the contents of a specific insertion point and instead, descendants
  of the host element can be styled selectively. Users can also create an
  extra node around an insertion point and style that node's contents
  via descendent selectors. For example, with a shadowRoot like this:

    <style>
      ::content(div) {
        background: red;
      }
    </style>
    <content></content>

  could become:

    <style>
      / *@polyfill .content-container div * /
      ::content(div) {
        background: red;
      }
    </style>
    <div class="content-container">
      <content></content>
    </div>

  Note the use of @polyfill in the comment above a ShadowDOM specific style
  declaration. This is a directive to the styling shim to use the selector
  in comments in lieu of the next selector when running under polyfill.
*/


const strictStyling = false;
const allowArraySelectors = false;
const isIeSupported = false;

// change a selector like 'div' to 'name div'
export function scopeRules(cssRules, scopeSelector, opt_transformer) {
    var cssText = '';
    if (cssRules) {
        Array.prototype.forEach.call(cssRules, function(rule) {
            if (rule.selectorText && (rule.style && rule.style.cssText !== undefined)) {
                cssText += doScopeSelector(
                    rule.selectorText, scopeSelector, opt_transformer) + ' {\n\t';
                cssText += propertiesFromRule(rule) + '\n}\n\n';
            } else if (rule.type === CSSRule.MEDIA_RULE) {
                cssText += '@media ' + rule.media.mediaText + ' {\n';
                cssText += scopeRules(rule.cssRules, scopeSelector);
                cssText += '\n}\n\n';
            } else {
                // KEYFRAMES_RULE in IE throws when we query cssText
                // when it contains a -webkit- property.
                // if this happens, we fallback to constructing the rule
                // from the CSSRuleSet
                // https://connect.microsoft.com/IE/feedbackdetail/view/955703/accessing-csstext-of-a-keyframe-rule-that-contains-a-webkit-property-via-cssom-generates-exception
                try {
                    if (rule.cssText) {
                        cssText += rule.cssText + '\n\n';
                    }
                } catch (x) {
                    if (
                        isIeSupported &&
                        rule.type === CSSRule.KEYFRAMES_RULE &&
                        rule.cssRules
                    ) {
                        cssText += ieSafeCssTextFromKeyFrameRule(rule);
                    }
                }
            }
        });
    }
    return cssText;
}

export function ieSafeCssTextFromKeyFrameRule(rule) {
    var cssText = '@keyframes ' + rule.name + ' {';
    Array.prototype.forEach.call(rule.cssRules, function(rule) {
        cssText += ' ' + rule.keyText + ' {' + rule.style.cssText + '}';
    });
    cssText += ' }';
    return cssText;
}

export function doScopeSelector(selector, scopeSelector, opt_transformer) {
    var r = [],
        parts = selector.split(',');
    parts.forEach(function(p) {
        p = p.trim();
        if (opt_transformer) {
            p = opt_transformer(p);
        }
        if (selectorNeedsScoping(p, scopeSelector)) {
            p = (strictStyling && !p.match(polyfillHostNoCombinator)) ?
                applyStrictSelectorScope(p, scopeSelector) :
                applySelectorScope(p, scopeSelector);
        }
        r.push(p);
    });
    return r.join(', ');
}

export function selectorNeedsScoping(selector, scopeSelector) {
    if (allowArraySelectors && Array.isArray(scopeSelector)) {
        return true;
    }
    var re = makeScopeMatcher(scopeSelector);
    return !selector.match(re);
}

export function makeScopeMatcher(scopeSelector) {
    scopeSelector = scopeSelector.replace(/\[/g, '\\[').replace(/\]/g, '\\]');
    return new RegExp('^(' + scopeSelector + ')' + selectorReSuffix, 'm');
}

export function applySelectorScope(selector, selectorScope) {
    return allowArraySelectors && Array.isArray(selectorScope) ?
        applySelectorScopeList(selector, selectorScope) :
        applySimpleSelectorScope(selector, selectorScope);
}

// apply an array of selectors
export function applySelectorScopeList(selector, scopeSelectorList) {
    var r = [];
    for (var i = 0, s;
        (s = scopeSelectorList[i]); i++) {
        r.push(applySimpleSelectorScope(selector, s));
    }
    return r.join(', ');
}

// scope via name and [is=name]
export function applySimpleSelectorScope(selector, scopeSelector) {
    if (selector.match(polyfillHostRe)) {
        selector = selector.replace(polyfillHostNoCombinator, scopeSelector);
        return selector.replace(polyfillHostRe, scopeSelector + ' ');
    } else {
        return scopeSelector + ' ' + selector;
    }
}

// return a selector with [name] suffix on each simple selector
// e.g. .foo.bar > .zot becomes .foo[name].bar[name] > .zot[name]
export function applyStrictSelectorScope(selector, scopeSelector) {
    scopeSelector = scopeSelector.replace(/\[is=([^\]]*)\]/g, '$1');
    var splits = [' ', '>', '+', '~'],
        scoped = selector,
        attrName = '[' + scopeSelector + ']';
    splits.forEach(function(sep) {
        var parts = scoped.split(sep);
        scoped = parts.map(function(p) {
            // remove :host since it should be unnecessary
            var t = p.trim().replace(polyfillHostRe, '');
            if (t && (splits.indexOf(t) < 0) && (t.indexOf(attrName) < 0)) {
                p = t.replace(/([^:]*)(:*)(.*)/, '$1' + attrName + '$2$3');
            }
            return p;
        }).join(sep);
    });
    return scoped;
}

export function propertiesFromRule(rule) {
    var cssText = rule.style.cssText;
    // TODO(sorvell): Safari cssom incorrectly removes quotes from the content
    // property. (https://bugs.webkit.org/show_bug.cgi?id=118045)
    // don't replace attr rules
    if (rule.style.content && !rule.style.content.match(/['"]+|attr/)) {
        cssText = cssText.replace(/content:[^;]*;/g, 'content: \'' +
            rule.style.content + '\';');
    }
    // TODO(sorvell): we can workaround this issue here, but we need a list
    // of troublesome properties to fix https://github.com/Polymer/platform/issues/53
    //
    // inherit rules can be omitted from cssText
    // TODO(sorvell): remove when Blink bug is fixed:
    // https://code.google.com/p/chromium/issues/detail?id=358273
    var style = rule.style;
    for (var i in style) {
        if (style[i] === 'initial') {
            cssText += i + ': initial; ';
        }
    }
    return cssText;
}

var selectorRe = /([^{]*)({[\s\S]*?})/gim,
    cssCommentRe = /\/\*[^*]*\*+([^/*][^*]*\*+)*\//gim,
    // TODO(sorvell): remove either content or comment
    cssCommentNextSelectorRe = /\/\*\s*@polyfill ([^*]*\*+([^/*][^*]*\*+)*\/)([^{]*?){/gim,
    cssContentNextSelectorRe = /polyfill-next-selector[^}]*content\:[\s]*?['"](.*?)['"][;\s]*}([^{]*?){/gim,
    // TODO(sorvell): remove either content or comment
    cssCommentRuleRe = /\/\*\s@polyfill-rule([^*]*\*+([^/*][^*]*\*+)*)\//gim,
    cssContentRuleRe = /(polyfill-rule)[^}]*(content\:[\s]*['"](.*?)['"])[;\s]*[^}]*}/gim,
    // TODO(sorvell): remove either content or comment
    cssCommentUnscopedRuleRe = /\/\*\s@polyfill-unscoped-rule([^*]*\*+([^/*][^*]*\*+)*)\//gim,
    cssContentUnscopedRuleRe = /(polyfill-unscoped-rule)[^}]*(content\:[\s]*['"](.*?)['"])[;\s]*[^}]*}/gim,
    cssPseudoRe = /::(x-[^\s{,(]*)/gim,
    cssPartRe = /::part\(([^)]*)\)/gim,
    // note: :host pre-processed to -shadowcsshost.
    polyfillHost = '-shadowcsshost',
    // note: :host-context pre-processed to -shadowcsshostcontext.
    polyfillHostContext = '-shadowcsscontext',
    parenSuffix = ')(?:\\((' +
    '(?:\\([^)(]*\\)|[^)(]*)+?' +
    ')\\))?([^,{]*)';
var cssColonHostRe = new RegExp('(' + polyfillHost + parenSuffix, 'gim'),
    cssColonHostContextRe = new RegExp('(' + polyfillHostContext + parenSuffix, 'gim'),
    selectorReSuffix = '([>\\s~+\[.,{:][\\s\\S]*)?$',
    colonHostRe = /\:host/gim,
    colonHostContextRe = /\:host-context/gim,
    /* host name without combinator */
    polyfillHostNoCombinator = polyfillHost + '-no-combinator',
    polyfillHostRe = new RegExp(polyfillHost, 'gim'),
    polyfillHostContextRe = new RegExp(polyfillHostContext, 'gim'),
    shadowDOMSelectorsRe = [
        />>>/g,
        /::shadow/g,
        /::content/g,
        // Deprecated selectors
        /\/deep\//g, // former >>>
        /\/shadow\//g, // former ::shadow
        /\/shadow-deep\//g, // former /deep/
        /\^\^/g, // former /shadow/
        /\^(?!=)/g // former /shadow-deep/
    ];