/*
 * Copyright (C) 2017 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.
 */

WI.SpreadsheetCSSStyleDeclarationEditor = class SpreadsheetCSSStyleDeclarationEditor extends WI.View
{
    constructor(delegate, style)
    {
        super();

        this.element.classList.add(WI.SpreadsheetCSSStyleDeclarationEditor.StyleClassName);

        this._delegate = delegate;
        this.style = style;
        this._propertyViews = [];

        this._focused = false;
        this._inlineSwatchActive = false;

        this._showsImplicitProperties = false;
        this._alwaysShowPropertyNames = new Set;
        this._propertyVisibilityMode = WI.SpreadsheetCSSStyleDeclarationEditor.PropertyVisibilityMode.ShowAll;
        this._hideFilterNonMatchingProperties = false;
        this._sortPropertiesByName = false;

        this._propertyPendingStartEditing = null;
        this._pendingAddBlankPropertyIndexOffset = NaN;
        this._filterText = null;

        this._anchorIndex = NaN;
        this._focusIndex = NaN;
    }

    // Public

    initialLayout()
    {
        if (!this._style)
            return;

        this.element.addEventListener("focus", () => { this.focused = true; }, true);
        this.element.addEventListener("blur", (event) => {
            let focusedElement = event.relatedTarget;
            if (focusedElement && this.element.contains(focusedElement))
                return;

            this.focused = false;
        }, true);

        this.element.addEventListener("keydown", this._handleKeyDown.bind(this));
        this.element.addEventListener("cut", this._handleCut.bind(this));
        this.element.addEventListener("copy", this._handleCopy.bind(this));
    }

    layout()
    {
        // Prevent layout of properties when one of them is being edited. A full layout resets
        // the focus, text selection, and completion state <http://webkit.org/b/182619>.
        if (this.editing && !this._propertyPendingStartEditing && isNaN(this._pendingAddBlankPropertyIndexOffset))
            return;

        super.layout();

        this.element.removeChildren();

        if (this._style)
            this._style.updatePropertiesModifiedState();

        let properties = this.propertiesToRender;

        // FIXME: Only re-layout properties that have been modified and preserve focus whenever possible.
        this._propertyViews = [];

        let propertyViewPendingStartEditing = null;
        for (let index = 0; index < properties.length; index++) {
            let property = properties[index];
            let propertyView = new WI.SpreadsheetStyleProperty(this, property, {selectable: true});
            propertyView.index = index;
            this.element.append(propertyView.element);
            this._propertyViews.push(propertyView);

            if (property === this._propertyPendingStartEditing)
                propertyViewPendingStartEditing = propertyView;
        }

        if (propertyViewPendingStartEditing) {
            propertyViewPendingStartEditing.startEditingName();
            this._propertyPendingStartEditing = null;
        }

        if (this._filterText)
            this.applyFilter(this._filterText);

        if (!isNaN(this._pendingAddBlankPropertyIndexOffset))
            this.addBlankProperty(this._propertyViews.length - 1 - this._pendingAddBlankPropertyIndexOffset);
        else if (this.hasSelectedProperties())
            this.selectProperties(this._anchorIndex, this._focusIndex);

        this._updateDebugLockStatus();
    }

    detached()
    {
        this._inlineSwatchActive = false;
        this.focused = false;

        for (let propertyView of this._propertyViews)
            propertyView.detached();

        super.detached();
    }

    get style()
    {
        return this._style;
    }

    set style(style)
    {
        if (this._style === style)
            return;

        if (this._style)
            this._style.removeEventListener(WI.CSSStyleDeclaration.Event.PropertiesChanged, this._propertiesChanged, this);

        this._style = style || null;

        if (this._style)
            this._style.addEventListener(WI.CSSStyleDeclaration.Event.PropertiesChanged, this._propertiesChanged, this);

        this.needsLayout();
    }

    get editing()
    {
        return this._focused || this._inlineSwatchActive;
    }

    set focused(value)
    {
        this._focused = value;
        this._updateStyleLock();
    }

    set inlineSwatchActive(value)
    {
        this._inlineSwatchActive = value;
        this._updateStyleLock();
    }

    set showsImplicitProperties(value)
    {
        if (value === this._showsImplicitProperties)
            return;

        this._showsImplicitProperties = value;

        this.needsLayout();
    }

    set alwaysShowPropertyNames(propertyNames)
    {
        this._alwaysShowPropertyNames = new Set(propertyNames);

        this.needsLayout();
    }

    set propertyVisibilityMode(propertyVisibilityMode)
    {
        if (this._propertyVisibilityMode === propertyVisibilityMode)
            return;

        this._propertyVisibilityMode = propertyVisibilityMode;

        this.needsLayout();
    }

    set hideFilterNonMatchingProperties(value)
    {
        if (value === this._hideFilterNonMatchingProperties)
            return;

        this._hideFilterNonMatchingProperties = value;

        this.needsLayout();
    }

    set sortPropertiesByName(value)
    {
        if (value === this._sortPropertiesByName)
            return;

        this._sortPropertiesByName = value;
        this.needsLayout();
    }

    get propertiesToRender()
    {
        let properties = [];
        if (!this._style)
            return properties;

        if (this._style._styleSheetTextRange)
            properties = this._style.visibleProperties;
        else
            properties = this._style.properties;

        if (this._style.inherited)
            properties = properties.filter((property) => property.inherited);

        if (this._sortPropertiesByName)
            properties.sort((a, b) => a.name.extendedLocaleCompare(b.name));

        let hideVariables = this._propertyVisibilityMode === SpreadsheetCSSStyleDeclarationEditor.PropertyVisibilityMode.HideVariables;
        let hideNonVariables = this._propertyVisibilityMode === SpreadsheetCSSStyleDeclarationEditor.PropertyVisibilityMode.HideNonVariables;

        return properties.filter((property) => {
            if (!property.isVariable && hideNonVariables)
                return false;

            if (property.isVariable && hideVariables)
                return false;

            return !property.implicit || this._showsImplicitProperties || this._alwaysShowPropertyNames.has(property.canonicalName);
        });
    }

    get selectionRange()
    {
        let startIndex = Math.min(this._anchorIndex, this._focusIndex);
        let endIndex = Math.max(this._anchorIndex, this._focusIndex);
        return [startIndex, endIndex];
    }

    startEditingFirstProperty()
    {
        let firstEditableProperty = this._editablePropertyAfter(-1);
        if (firstEditableProperty)
            firstEditableProperty.startEditingName();
        else {
            const appendAfterLast = -1;
            this.addBlankProperty(appendAfterLast);
        }
    }

    startEditingLastProperty()
    {
        let lastEditableProperty = this._editablePropertyBefore(this._propertyViews.length);
        if (lastEditableProperty)
            lastEditableProperty.startEditingValue();
        else {
            const appendAfterLast = -1;
            this.addBlankProperty(appendAfterLast);
        }
    }

    highlightProperty(property)
    {
        let propertiesMatch = (cssProperty) => {
            if (cssProperty.attached && !cssProperty.overridden) {
                if (cssProperty.canonicalName === property.canonicalName || hasMatchingLonghandProperty(cssProperty))
                    return true;
            }

            return false;
        };

        let hasMatchingLonghandProperty = (cssProperty) => {
            let cssProperties = cssProperty.relatedLonghandProperties;

            if (!cssProperties.length)
                return false;

            for (let property of cssProperties) {
                if (propertiesMatch(property))
                    return true;
            }

            return false;
        };

        for (let i = 0; i < this._propertyViews.length; ++i) {
            if (propertiesMatch(this._propertyViews[i].property)) {
                this.selectProperties(i, i);
                return true;
            }
        }

        return false;
    }

    addBlankProperty(index)
    {
        this._pendingAddBlankPropertyIndexOffset = NaN;

        if (index === -1) {
            // Append to the end.
            index = this._propertyViews.length;
        }

        this._propertyPendingStartEditing = this._style.newBlankProperty(index);
        this.needsLayout();
    }

    hasSelectedProperties()
    {
        return !isNaN(this._anchorIndex) && !isNaN(this._focusIndex);
    }

    selectProperties(anchorIndex, focusIndex)
    {
        console.assert(anchorIndex < this._propertyViews.length, `anchorIndex (${anchorIndex}) is greater than the last property index (${this._propertyViews.length})`);
        console.assert(focusIndex < this._propertyViews.length, `focusIndex (${focusIndex}) is greater than the last property index (${this._propertyViews.length})`);

        if (isNaN(anchorIndex) || isNaN(focusIndex)) {
            console.error(`Nothing to select. anchorIndex (${anchorIndex}) and focusIndex (${focusIndex}) must be numbers.`);
            this.deselectProperties();
            return;
        }

        this._anchorIndex = anchorIndex;
        this._focusIndex = focusIndex;

        let [startIndex, endIndex] = this.selectionRange;

        for (let i = 0; i < this._propertyViews.length; ++i) {
            let propertyView = this._propertyViews[i];
            let isSelected = i >= startIndex && i <= endIndex;
            propertyView.selected = isSelected;
        }

        this._suppressBlur = true;
        let property = this._propertyViews[focusIndex];
        property.element.focus();
        this._suppressBlur = false;
    }

    extendSelectedProperties(focusIndex)
    {
        this.selectProperties(this._anchorIndex, focusIndex);
    }

    deselectProperties()
    {
        for (let propertyView of this._propertyViews)
            propertyView.selected = false;

        this._focused = false;
        this._anchorIndex = NaN;
        this._focusIndex = NaN;
    }

    placeTextCaretInFocusedProperty()
    {
        if (isNaN(this._focusIndex))
            return;

        let focusedProperty = this._propertyViews[this._focusIndex];
        let selection = window.getSelection();
        selection.removeAllRanges();
        selection.setBaseAndExtent(focusedProperty.element, 0, focusedProperty.element, 0);
    }

    applyFilter(filterText)
    {
        this._filterText = filterText;

        if (!this.didInitialLayout)
            return;

        let matches = false;
        for (let propertyView of this._propertyViews) {
            if (propertyView.applyFilter(this._filterText)) {
                matches = true;

                if (this._hideFilterNonMatchingProperties)
                    this.element.append(propertyView.element);
            } else if (this._hideFilterNonMatchingProperties)
                propertyView.element.remove();
        }

        this.dispatchEventToListeners(WI.SpreadsheetCSSStyleDeclarationEditor.Event.FilterApplied, {matches});
    }

    // SpreadsheetStyleProperty delegate

    spreadsheetStylePropertyBlur(event, property)
    {
        if (this._suppressBlur)
            return;

        if (this._delegate.spreadsheetCSSStyleDeclarationEditorPropertyBlur)
            this._delegate.spreadsheetCSSStyleDeclarationEditorPropertyBlur(event, property);
    }

    spreadsheetStylePropertyMouseEnter(event, property)
    {
        if (this._delegate.spreadsheetCSSStyleDeclarationEditorPropertyMouseEnter)
            this._delegate.spreadsheetCSSStyleDeclarationEditorPropertyMouseEnter(event, property);
    }

    spreadsheetStylePropertyFocusMoved(propertyView, {direction, willRemoveProperty})
    {
        this._updatePropertiesStatus();

        if (!direction)
            return;

        let movedFromIndex = this._propertyViews.indexOf(propertyView);
        console.assert(movedFromIndex !== -1, "Property doesn't exist, focusing on a selector as a fallback.");
        if (movedFromIndex === -1) {
            if (this._style.selectorEditable)
                this._delegate.spreadsheetCSSStyleDeclarationEditorStartEditingRuleSelector();

            return;
        }

        if (direction === "forward") {
            // Move from the value to the next enabled property's name.
            let propertyView = this._editablePropertyAfter(movedFromIndex);
            if (propertyView)
                propertyView.startEditingName();
            else {
                if (willRemoveProperty) {
                    const delta = 1;
                    this._delegate.spreadsheetCSSStyleDeclarationEditorStartEditingAdjacentRule(this, delta);
                } else {
                    const appendAfterLast = -1;
                    this.addBlankProperty(appendAfterLast);
                }
            }
        } else {
            let propertyView = this._editablePropertyBefore(movedFromIndex);
            if (propertyView) {
                // Move from the property's name to the previous enabled property's value.
                propertyView.startEditingValue();
            } else {
                // Move from the first property's name to the rule's selector.
                if (this._style.selectorEditable)
                    this._delegate.spreadsheetCSSStyleDeclarationEditorStartEditingRuleSelector();
                else {
                    const delta = -1;
                    this._delegate.spreadsheetCSSStyleDeclarationEditorStartEditingAdjacentRule(this, delta);
                }
            }
        }
    }

    spreadsheetStylePropertySelect(index)
    {
        this.selectProperties(index, index);
    }

    spreadsheetStylePropertySelectByProperty(property)
    {
        if (this._delegate && this._delegate.spreadsheetCSSStyleDeclarationEditorSelectProperty)
            this._delegate.spreadsheetCSSStyleDeclarationEditorSelectProperty(property);
    }

    spreadsheetStylePropertyAddBlankPropertySoon(propertyView, {index})
    {
        if (isNaN(index))
            index = this._propertyViews.length;
        this._pendingAddBlankPropertyIndexOffset = this._propertyViews.length - index;
    }

    spreadsheetStylePropertyWillRemove(propertyView)
    {
        this._propertyViews.remove(propertyView);

        for (let index = 0; index < this._propertyViews.length; index++)
            this._propertyViews[index].index = index;

        let wasFocused = document.activeElement && propertyView.element.contains(document.activeElement);
        if (wasFocused)
            this.focused = false;
    }

    spreadsheetStylePropertyShowProperty(propertyView, property)
    {
        if (this._delegate.spreadsheetCSSStyleDeclarationEditorShowProperty)
            this._delegate.spreadsheetCSSStyleDeclarationEditorShowProperty(this, property);
    }

    spreadsheetStylePropertyDidPressEsc(propertyView)
    {
        let index = this._propertyViews.indexOf(propertyView);
        console.assert(index !== -1, `Can't find StyleProperty to select (${propertyView.property.name})`);
        if (index !== -1)
            this.selectProperties(index, index);
    }

    stylePropertyInlineSwatchActivated()
    {
        this.inlineSwatchActive = true;
    }

    stylePropertyInlineSwatchDeactivated()
    {
        this.inlineSwatchActive = false;
    }

    // Private

    _handleKeyDown(event)
    {
        if (!this.hasSelectedProperties() || !this._propertyViews.length)
            return;

        if (event.key === "ArrowUp" || event.key === "ArrowDown") {
            let delta = event.key === "ArrowUp" ? -1 : 1;
            let focusIndex = Number.constrain(this._focusIndex + delta, 0, this._propertyViews.length - 1);

            if (event.shiftKey)
                this.extendSelectedProperties(focusIndex);
            else
                this.selectProperties(focusIndex, focusIndex);

            event.stop();
        } else if (event.key === "Tab" || event.key === "Enter") {
            if (!this.style.editable)
                return;

            let property = this._propertyViews[this._focusIndex];
            if (property && property.enabled) {
                event.stop();
                property.startEditingName();
            }
        } else if (event.key === "Backspace" || event.key === "Delete") {
            if (!this.style.editable) {
                InspectorFrontendHost.beep();
                return;
            }

            this._removeSelectedProperties();
            event.stop();
        } else if ((event.code === "Space" && !event.shiftKey && !event.metaKey && !event.ctrlKey) || (event.key === "/" && event.commandOrControlKey && !event.shiftKey)) {
            if (!this.style.editable)
                return;

            let [startIndex, endIndex] = this.selectionRange;

            // Toggle the first selected property and set this state to all selected properties.
            let disabled = this._propertyViews[startIndex].property.enabled;

            for (let i = endIndex; i >= startIndex; --i) {
                let propertyView = this._propertyViews[i];
                propertyView.property.commentOut(disabled);
                propertyView.update();
            }

            event.stop();

        } else if (event.key === "a" && event.commandOrControlKey) {

            this.selectProperties(0, this._propertyViews.length - 1);
            event.stop();

        } else if (event.key === "Esc")
            this.deselectProperties();
    }

    _handleCopy(event)
    {
        if (!this.hasSelectedProperties())
            return;

        this._copySelectedProperties(event);
    }

    _handleCut(event)
    {
        if (!this.style.editable) {
            InspectorFrontendHost.beep();
            return;
        }

        if (!this.hasSelectedProperties())
            return;

        this._copySelectedProperties(event);
        this._removeSelectedProperties();
    }

    _copySelectedProperties(event)
    {
        let [startIndex, endIndex] = this.selectionRange;
        let formattedProperties = this._propertyViews.slice(startIndex, endIndex + 1).map((propertyView) => propertyView.property.formattedText);
        event.clipboardData.setData("text/plain", formattedProperties.join("\n"));
        event.stop();
    }

    _removeSelectedProperties()
    {
        console.assert(this.style.editable);
        let [startIndex, endIndex] = this.selectionRange;

        let propertyIndexToSelect = NaN;
        if (endIndex + 1 !== this._propertyViews.length)
            propertyIndexToSelect = startIndex;
        else if (startIndex > 0)
            propertyIndexToSelect = startIndex - 1;

        this.deselectProperties();

        for (let i = endIndex; i >= startIndex; i--)
            this._propertyViews[i].remove();

        if (!isNaN(propertyIndexToSelect))
            this.selectProperties(propertyIndexToSelect, propertyIndexToSelect);
    }

    _editablePropertyAfter(propertyIndex)
    {
        for (let index = propertyIndex + 1; index < this._propertyViews.length; index++) {
            let property = this._propertyViews[index];
            if (property.enabled)
                return property;
        }

        return null;
    }

    _editablePropertyBefore(propertyIndex)
    {
        for (let index = propertyIndex - 1; index >= 0; index--) {
            let property = this._propertyViews[index];
            if (property.enabled)
                return property;
        }

        return null;
    }

    _propertiesChanged(event)
    {
        if (this.editing && isNaN(this._pendingAddBlankPropertyIndexOffset))
            this._updatePropertiesStatus();
        else
            this.needsLayout();
    }

    _updatePropertiesStatus()
    {
        for (let propertyView of this._propertyViews)
            propertyView.updateStatus();
    }

    _updateStyleLock()
    {
        if (!this._style)
            return;

        this._style.locked = this._focused || this._inlineSwatchActive;
        this._updateDebugLockStatus();
    }

    _updateDebugLockStatus()
    {
        if (!this._style || !WI.settings.debugEnableStyleEditingDebugMode.value)
            return;

        this.element.classList.toggle("debug-style-locked", this._style.locked);
    }
};

WI.SpreadsheetCSSStyleDeclarationEditor.Event = {
    FilterApplied: "spreadsheet-css-style-declaration-editor-filter-applied",
};

WI.SpreadsheetCSSStyleDeclarationEditor.StyleClassName = "spreadsheet-style-declaration-editor";

WI.SpreadsheetCSSStyleDeclarationEditor.PropertyVisibilityMode = {
    ShowAll: Symbol("variable-visibility-show-all"),
    HideVariables: Symbol("variable-visibility-hide-variables"),
    HideNonVariables: Symbol("variable-visibility-hide-non-variables"),
};
