/*
 * Copyright (c) 2008-2016 Haulmont.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package com.haulmont.cuba.desktop.gui.components;

import com.haulmont.chile.core.datatypes.Datatype;
import com.haulmont.chile.core.model.MetaProperty;
import com.haulmont.cuba.core.global.AppBeans;
import com.haulmont.cuba.core.global.MetadataTools;
import com.haulmont.cuba.desktop.App;
import com.haulmont.cuba.desktop.gui.data.ComponentSize;
import com.haulmont.cuba.desktop.gui.executors.impl.DesktopBackgroundWorker;
import com.haulmont.cuba.desktop.theme.DesktopTheme;
import com.haulmont.cuba.gui.ComponentsHelper;
import com.haulmont.cuba.gui.components.*;
import com.haulmont.cuba.gui.components.Formatter;
import com.haulmont.cuba.gui.icons.Icons;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.dom4j.Element;

import javax.annotation.Nullable;
import javax.swing.*;
import java.util.*;
import java.util.function.Consumer;

public abstract class DesktopAbstractComponent<C extends JComponent>
        implements
            DesktopComponent, Component.Wrapper, Component.HasXmlDescriptor, Component.BelongToFrame, Component.HasIcon, HasDebugId {

    public static final String SWING_PROPERTY_CLASS = "cubaClass";
    public static final String SWING_PROPERTY_ID = "cubaId";

    protected C impl;

    protected DesktopContainer container;

    protected String id;
    protected Frame frame;
    protected Component parent;

    protected Element xmlDescriptor;

    protected ComponentSize widthSize;
    protected ComponentSize heightSize;

    protected Alignment alignment;

    protected boolean visible = true;
    protected boolean enabled = true;
    protected boolean responsive = false;

    protected boolean parentEnabled = true;

    protected int tabIndex = 0; // just stub

    protected String debugId;

    // lazily initialized list
    protected List<String> styles;

    protected String caption;

    protected C getImpl() {
        return impl;
    }

    protected String getSwingPropertyId() {
        return SWING_PROPERTY_ID;
    }

    public String getCaption() {
        return caption;
    }

    public void setCaption(String caption) {
        if (!Objects.equals(getCaption(), caption)) {
            this.caption = caption;

            setCaptionToComponent(caption);

            if (this instanceof Field
                    && parent instanceof DesktopFieldGroup) {
                ((DesktopFieldGroup) parent).updateCaptionText(this);
            }
        }
    }

    protected void setCaptionToComponent(String caption) {
    }

    @Override
    public Component getParent() {
        return parent;
    }

    @Override
    public void setParent(Component parent) {
        this.parent = parent;
    }

    @Override
    public Frame getFrame() {
        return frame;
    }

    @Override
    public void setFrame(Frame frame) {
        this.frame = frame;
        if (frame != null) {
            frame.registerComponent(this);
        }

        assignAutoDebugId();
    }

    @Override
    public String getId() {
        return id;
    }

    @Override
    public void setId(String id) {
        if (!Objects.equals(this.id, id)) {
            if (frame != null) {
                frame.unregisterComponent(this);
            }

            this.id = id;
            C impl = getImpl();
            if (impl != null) {
                impl.putClientProperty(getSwingPropertyId(), id);
                impl.setName(id);
            }

            if (frame != null) {
                frame.registerComponent(this);
            }
        }
    }

    public void assignAutoDebugId() {
        App app = App.getInstance();
        if (app != null && app.isTestMode()) {
            C impl = getImpl();
            // always change name, do not assign auto id for components
            if (getId() == null && impl != null) {
                String alternativeDebugId = getAlternativeDebugId();

                impl.setName(alternativeDebugId);
            }
        }
    }

    /**
     * @return id that is suitable for auto debug id
     */
    protected String getAlternativeDebugId() {
        if (id != null) {
            return id;
        }

        return getClass().getSimpleName();
    }

    protected void assignClassDebugProperty(JComponent c) {
        c.putClientProperty(SWING_PROPERTY_CLASS, getClass().getSimpleName());
    }

    @Override
    public String getDebugId() {
        return debugId;
    }

    @Override
    public void setDebugId(String id) {
        this.debugId = id;
    }

    @Override
    public boolean isEnabled() {
        return this.enabled;
    }

    @Override
    public boolean isEnabledRecursive() {
        return DesktopComponentsHelper.isRecursivelyEnabled(getComposition());
    }

    @Override
    public void setEnabled(boolean enabled) {
        DesktopBackgroundWorker.checkSwingUIAccess();

        if (this.enabled != enabled) {
            this.enabled = enabled;

            updateEnabled();
        }
    }

    @Override
    public boolean isResponsive() {
        return responsive;
    }

    @Override
    public void setResponsive(boolean responsive) {
        this.responsive = responsive;
    }

    protected void updateEnabled() {
        getComposition().setEnabled(isEnabledWithParent());

        requestContainerUpdate();

        if (parent instanceof DesktopFieldGroup) {
            ((DesktopFieldGroup) parent).updateChildEnabled(this);
        }
    }

    protected boolean isEnabledWithParent() {
        return enabled && parentEnabled;
    }

    public boolean isComponentVisible() {
        return getComposition().isVisible();
    }

    @Override
    public boolean isVisible() {
        return this.visible;
    }

    @Override
    public boolean isVisibleRecursive() {
        return DesktopComponentsHelper.isRecursivelyVisible(getComposition());
    }

    @Override
    public void setVisible(boolean visible) {
        DesktopBackgroundWorker.checkSwingUIAccess();

        if (this.visible != visible) {
            this.visible = visible;

            getComposition().setVisible(visible);

            if (parent instanceof DesktopFieldGroup) {
                ((DesktopFieldGroup) parent).updateCaptionVisibility(this);
            }

            requestContainerUpdate();
        }
    }

    public void focus() {
        SwingUtilities.invokeLater(() ->
                impl.requestFocus()
        );
    }

    @Override
    public float getHeight() {
        return heightSize != null ? heightSize.value : -1;
    }

    @Override
    public int getHeightUnits() {
        return ComponentsHelper.convertFromSizeUnit(getHeightSizeUnit());
    }

    @Override
    public SizeUnit getHeightSizeUnit() {
        return heightSize != null ? heightSize.sizeUnit : SizeUnit.PIXELS;
    }

    @Override
    public void setHeight(String height) {
        heightSize = ComponentSize.parse(height);
        requestContainerUpdate();
    }

    @Override
    public float getWidth() {
        return widthSize != null ? widthSize.value : -1;
    }

    @Override
    public int getWidthUnits() {
        return ComponentsHelper.convertFromSizeUnit(getWidthSizeUnit());
    }

    @Override
    public SizeUnit getWidthSizeUnit() {
        return widthSize != null ? widthSize.sizeUnit : SizeUnit.PIXELS;
    }

    @Override
    public void setWidth(String width) {
        widthSize = ComponentSize.parse(width);
        requestContainerUpdate();
    }

    @Override
    public Alignment getAlignment() {
        return alignment;
    }

    @Override
    public void setAlignment(Alignment alignment) {
        if (this.alignment != alignment) {
            this.alignment = alignment;
            requestContainerUpdate();
        }
    }

    @Override
    public String getStyleName() {
        if (CollectionUtils.isEmpty(styles))
            return StringUtils.EMPTY;

        return String.join(" ", styles);
    }

    @Override
    public void setStyleName(String styleName) {
        if (styles == null)
            styles = new LinkedList<>();

        styles.clear();

        parseAndApplyTheme(styleName);
    }

    @Override
    public void addStyleName(String styleName) {
        if (styles == null)
            styles = new LinkedList<>();

        if (StringUtils.isEmpty(styleName) || styles.contains(styleName))
            return;

        parseAndApplyTheme(styleName);
    }

    protected void parseAndApplyTheme(String styleName) {
        String style = null;
        if (StringUtils.isNotEmpty(styleName)) {
            StringTokenizer tokenizer = new StringTokenizer(styleName, " ");
            while (tokenizer.hasMoreTokens()) {
                style = tokenizer.nextToken();
                styles.add(style);
            }
        }

        DesktopTheme appTheme = App.getInstance().getTheme();
        if (appTheme != null)
            appTheme.applyStyle(this, style);
    }

    @Override
    public void removeStyleName(String styleName) {
        if (StringUtils.isEmpty(styleName) || CollectionUtils.isEmpty(styles) || !styles.contains(styleName))
            return;

        List<String> passedStyles = new ArrayList<>();

        StringTokenizer tokenizer = new StringTokenizer(styleName, " ");
        while (tokenizer.hasMoreTokens()) {
            passedStyles.add(tokenizer.nextToken());
        }

        String appliedStyleName = styles.get(styles.size() - 1);
        if (passedStyles.contains(appliedStyleName)) {
            DesktopTheme appTheme = App.getInstance().getTheme();
            if (appTheme != null)
                appTheme.applyStyle(this, null);
        }

        styles.removeAll(passedStyles);
    }

    @Override
    public Element getXmlDescriptor() {
        return xmlDescriptor;
    }

    @Override
    public void setXmlDescriptor(Element element) {
        xmlDescriptor = element;
    }

    @Override
    public JComponent getComponent() {
        return impl;
    }

    @Override
    public JComponent getComposition() {
        return impl;
    }

    @Override
    public void setContainer(DesktopContainer container) {
        this.container = container;
    }

    protected void requestContainerUpdate() {
        if (container != null) {
            container.updateComponent(this);
        }
    }

    @Override
    public void setExpanded(boolean expanded) {
    }

    public boolean isParentEnabled() {
        return parentEnabled;
    }

    public void setParentEnabled(boolean parentEnabled) {
        this.parentEnabled = parentEnabled;

        updateEnabled();
    }

    /**
     * Default formatter for {@link DesktopLabel} and {@link DesktopTextField}
     */
    protected static class DefaultValueFormatter {

        private MetaProperty metaProperty;
        private Formatter formatter;
        private Datatype datatype;
        private Locale locale;

        public DefaultValueFormatter(Locale locale) {
            this.locale = locale;
        }

        public Formatter getFormatter() {
            return formatter;
        }

        public void setFormatter(Formatter formatter) {
            this.formatter = formatter;
        }

        public Datatype getDatatype() {
            return datatype;
        }

        public void setDatatype(Datatype datatype) {
            this.datatype = datatype;
        }

        public MetaProperty getMetaProperty() {
            return metaProperty;
        }

        public void setMetaProperty(MetaProperty metaProperty) {
            this.metaProperty = metaProperty;
        }

        /**
         * Format value for text field or label
         *
         * @param value Object value
         * @return Formatted string
         */
        public String formatValue(Object value) {
            String text;
            if (formatter == null) {
                if (value == null) {
                    text = "";
                } else {
                    MetadataTools metadataTools = AppBeans.get(MetadataTools.NAME);

                    if (metaProperty != null) {
                        text = metadataTools.format(value, metaProperty);
                    } else if (datatype != null) {
                        text = datatype.format(value, locale);
                    } else {
                        text = metadataTools.format(value);
                    }
                }
            } else {
                text = formatter.format(value);
            }
            return text;
        }
    }

    @SuppressWarnings("unchecked")
    @Override
    public <X> X unwrap(Class<X> internalComponentClass) {
        return (X) getComponent();
    }

    @Nullable
    @Override
    public <X> X unwrapOrNull(Class<X> internalComponentClass) {
        return internalComponentClass.isAssignableFrom(getComponent().getClass())
                ? internalComponentClass.cast(getComponent())
                : null;
    }

    @Override
    public <X> void withUnwrapped(Class<X> internalComponentClass, Consumer<X> action) {
        if (internalComponentClass.isAssignableFrom(getComponent().getClass())) {
            action.accept(internalComponentClass.cast(getComponent()));
        }
    }

    @SuppressWarnings("unchecked")
    @Override
    public <X> X unwrapComposition(Class<X> internalCompositionClass) {
        return (X) getComposition();
    }

    @Nullable
    @Override
    public <X> X unwrapCompositionOrNull(Class<X> internalCompositionClass) {
        return internalCompositionClass.isAssignableFrom(getComposition().getClass())
                ? internalCompositionClass.cast(getComposition())
                : null;
    }

    @Override
    public <X> void withUnwrappedComposition(Class<X> internalCompositionClass, Consumer<X> action) {
        if (internalCompositionClass.isAssignableFrom(getComposition().getClass())) {
            action.accept(internalCompositionClass.cast(getComposition()));
        }
    }

    @Override
    public String getIcon() {
        return null;
    }

    @Override
    public void setIcon(String icon) {
        // do nothing
    }

    @Override
    public void setIconFromSet(Icons.Icon icon) {
        // do nothing
    }

    // Just stub
    public int getTabIndex() {
        return tabIndex;
    }

    // Just stub
    public void setTabIndex(int tabIndex) {
        this.tabIndex = tabIndex;
    }
}