/*
 * Inspector wrapper base class.
 */
+function ($) { "use strict";

    // NAMESPACES
    // ============================

    if ($.oc.inspector === undefined)
        $.oc.inspector = {}

    if ($.oc.inspector.wrappers === undefined)
        $.oc.inspector.wrappers = {}

    // CLASS DEFINITION
    // ============================

    var Base = $.oc.foundation.base,
        BaseProto = Base.prototype

    var BaseWrapper = function($element, sourceWrapper, options) {
        this.$element = $element

        this.options = $.extend({}, BaseWrapper.DEFAULTS, typeof options == 'object' && options)
        this.switched = false
        this.configuration = null

        Base.call(this)

        if (!sourceWrapper) {
            if (!this.triggerShowingAndInit()) {
                // this.init() is called inside triggerShowing()

                return
            }

            this.surface = null
            this.title = null
            this.description = null
        } 
        else {
            this.surface = sourceWrapper.surface
            this.title = sourceWrapper.title
            this.description = sourceWrapper.description

            sourceWrapper = null

            this.init()
        }
    }

    BaseWrapper.prototype = Object.create(BaseProto)
    BaseWrapper.prototype.constructor = Base

    BaseWrapper.prototype.dispose = function() {
        if (!this.switched) {
            this.$element.removeClass('inspector-open')
            this.setInspectorVisibleFlag(false)

            this.$element.trigger('hidden.oc.inspector')
        }

        if (this.surface !== null && this.surface.options.onGetInspectableElement === this.proxy(this.onGetInspectableElement)) {
            this.surface.options.onGetInspectableElement = null
        }

        this.surface = null
        this.$element = null
        this.title = null
        this.description = null
        this.configuration = null

        BaseProto.dispose.call(this)
    }

    BaseWrapper.prototype.init = function() {
        // Wrappers can create a new surface or inject an existing
        // surface to the UI they manage.
        //
        // If there is no surface provided in the wrapper constructor,
        // the wrapper first loads the Inspector configuration and values
        // and then calls the createSurfaceAndUi() method with all information
        // required for creating a new Inspector surface and UI.

        if (!this.surface) {
            this.loadConfiguration()
        }
        else {
            this.adoptSurface()
        }

        this.$element.addClass('inspector-open')
    }

    //
    // Helper methods
    //

    BaseWrapper.prototype.getElementValuesInput = function() {
        return this.$element.find('> input[data-inspector-values]')
    }

    BaseWrapper.prototype.normalizePropertyCode = function(code, configuration) {
        var lowerCaseCode = code.toLowerCase()

        for (var index in configuration) {
            var propertyInfo = configuration[index]

            if (propertyInfo.property.toLowerCase() == lowerCaseCode) {
                return propertyInfo.property
            }
        }

        return code
    }

    BaseWrapper.prototype.isExternalParametersEditorEnabled = function() {
        return this.$element.closest('[data-inspector-external-parameters]').length > 0
    }

    BaseWrapper.prototype.initSurface = function(containerElement, properties, values) {
        var options = this.$element.data() || {}

        options.enableExternalParameterEditor = this.isExternalParametersEditorEnabled()
        options.onGetInspectableElement = this.proxy(this.onGetInspectableElement)

        this.surface = new $.oc.inspector.surface(
            containerElement,
            properties,
            values,
            $.oc.inspector.helpers.generateElementUniqueId(this.$element.get(0)),
            options)
    }

    BaseWrapper.prototype.isLiveUpdateEnabled = function() {
        return false
    }

    //
    // Wrapper API
    //

    BaseWrapper.prototype.createSurfaceAndUi = function(properties, values) {

    }

    BaseWrapper.prototype.setInspectorVisibleFlag = function(value) {
        this.$element.data('oc.inspectorVisible', value)
    }

    BaseWrapper.prototype.adoptSurface = function() {
        this.surface.options.onGetInspectableElement = this.proxy(this.onGetInspectableElement)
    }

    BaseWrapper.prototype.cleanupAfterSwitch = function() {
        this.switched = true
        this.dispose()
    }

    //
    // Values
    //

    BaseWrapper.prototype.loadValues = function(configuration) {
        var $valuesField = this.getElementValuesInput()

        if ($valuesField.length > 0) {
            var valuesStr = $.trim($valuesField.val())

            try {
                return valuesStr.length === 0 ? {} : JSON.parse(valuesStr)
            }
            catch (err) {
                throw new Error('Error parsing Inspector field values. ' + err)
            }
        }

        var values = {},
            attributes = this.$element.get(0).attributes

        for (var i=0, len = attributes.length; i < len; i++) {
            var attribute = attributes[i],
                matches = []

            if (matches = attribute.name.match(/^data-property-(.*)$/)) {
                // Important - values contained in data-property-xxx attributes are
                // considered strings and never parsed with JSON. The use of the
                // data-property-xxx attributes is very limited - they're only
                // used in Pages for creating snippets from partials, where properties 
                // are created with a table UI widget, which doesn't allow creating 
                // properties of any complex types.
                //
                // There is no a technically reliable way to determine when a string
                // is a JSON data or a regular string. Users can enter a value
                // like [10], which is a proper JSON value, but meant to be a string.
                //
                // One possible way to resolve it, if to check the property type loaded
                // from the configuration and see if the corresponding editor expects
                // complex data.

                var normalizedPropertyName = this.normalizePropertyCode(matches[1], configuration)
                values[normalizedPropertyName] = attribute.value
            }
        }

        return values
    }

    BaseWrapper.prototype.applyValues = function(liveUpdateMode) {
        var $valuesField = this.getElementValuesInput(),
            values = liveUpdateMode ? 
                        this.surface.getValidValues() : 
                        this.surface.getValues()

        if (liveUpdateMode) {
            // In the live update mode, when only valid values are applied,
            // we don't want to change all other values (invalid properties).

            var existingValues = this.loadValues(this.configuration)

            for (var property in values) {
                if (values[property] !== $.oc.inspector.invalidProperty) {
                    existingValues[property] = values[property]
                }
            }

            // Properties that use settings like ignoreIfPropertyEmpty could 
            // be removed from the list returned by getValidValues(). Removed
            // properties should be removed from the result list.

            var filteredValues = {}

            for (var property in existingValues) {
                if (values.hasOwnProperty(property)) {
                    filteredValues[property] = existingValues[property]
                }
            }


            values = filteredValues
        }

        if ($valuesField.length > 0) {
            $valuesField.val(JSON.stringify(values))
        } 
        else {
            for (var property in values) {
                var value = values[property]

                if ($.isArray(value) ||$.isPlainObject(value)) {
                    throw new Error('Inspector data-property-xxx attributes do not support complex values. Property: ' + property)
                }

                this.$element.attr('data-property-' + property, value)
            }
        }

        // In the live update mode the livechange event is triggered 
        // regardless of whether Surface properties match or don't match
        // the original properties of the inspectable element. Without it
        // there could be undesirable side effects.

        if (liveUpdateMode) {
            this.$element.trigger('livechange')
        }
        else {
            var hasChanges = false

            if (this.isLiveUpdateEnabled()) {
                var currentValues = this.loadValues(this.configuration)

                // If the Inspector setup supports the live update mode,
                // evaluate changes as a difference between the current element
                // properties and internal properties stored in the Surface.
                // If there is no differences, the properties have already
                // been applied with a preceding live update.
                hasChanges = this.surface.hasChanges(currentValues)
            }
            else {
                hasChanges = this.surface.hasChanges()
            }

            if (hasChanges) {
                this.$element.trigger('change')
            }
        }
    }

    //
    // Configuration
    //

    BaseWrapper.prototype.loadConfiguration = function() {
        var configString = this.$element.data('inspector-config'),
            result = {
                properties: {},
                title: null,
                description: null
            }

        result.title = this.$element.data('inspector-title')
        result.description = this.$element.data('inspector-description')

        if (configString !== undefined) {
            result.properties = this.parseConfiguration(configString)

            this.configurationLoaded(result)
            return
        }

        var $configurationField = this.$element.find('> input[data-inspector-config]')

        if ($configurationField.length > 0) {
            result.properties = this.parseConfiguration($configurationField.val())

            this.configurationLoaded(result)
            return
        }

        var $form = this.$element.closest('form'),
            data = this.$element.data(),
            self = this

        $.oc.stripeLoadIndicator.show()
        $form.request('onGetInspectorConfiguration', {
            data: data
        }).done(function inspectorConfigurationRequestDoneClosure(data) {
            self.onConfigurartionRequestDone(data, result)
        }).always(function() {
            $.oc.stripeLoadIndicator.hide()
        })
    }

    BaseWrapper.prototype.parseConfiguration = function(configuration) {
        if (!$.isArray(configuration) && !$.isPlainObject(configuration)) {
            if ($.trim(configuration) === 0) {
                return {}
            }

            try {
               return JSON.parse(configuration)
            }
            catch(err) {
                throw new Error('Error parsing Inspector configuration. ' + err)
            }
        }
        else {
            return configuration
        }
    }

    BaseWrapper.prototype.configurationLoaded = function(configuration) {
        var values = this.loadValues(configuration.properties)

        this.title = configuration.title
        this.description = configuration.description
        this.configuration = configuration

        this.createSurfaceAndUi(configuration.properties, values)
    }

    BaseWrapper.prototype.onConfigurartionRequestDone = function(data, result) {
        result.properties = this.parseConfiguration(data.configuration.properties)

        if (data.configuration.title !== undefined) {
            result.title = data.configuration.title
        }

        if (data.configuration.description !== undefined) {
            result.description = data.configuration.description
        }

        this.configurationLoaded(result)
    }

    //
    // Events
    //

    BaseWrapper.prototype.triggerShowingAndInit = function() {
        var e = $.Event('showing.oc.inspector')

        this.$element.trigger(e, [{callback: this.proxy(this.init)}])
        if (e.isDefaultPrevented()) {
            this.$element = null

            return false
        }

        if (!e.isPropagationStopped()) {
            this.init()
        }
    }

    BaseWrapper.prototype.triggerHiding = function() {
        var hidingEvent = $.Event('hiding.oc.inspector'),
            values = this.surface.getValues()

        this.$element.trigger(hidingEvent, [{values: values}])
        return !hidingEvent.isDefaultPrevented();
    }

    BaseWrapper.prototype.onGetInspectableElement = function() {
        return this.$element
    }

    BaseWrapper.DEFAULTS = {
        containerSupported: false
    }

    $.oc.inspector.wrappers.base = BaseWrapper
}(window.jQuery);