/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.syncope.client.console.wicket.markup.html.form;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import org.apache.commons.collections4.ListUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.syncope.client.console.commons.Constants;
import org.apache.syncope.client.console.wicket.ajax.form.IndicatorAjaxFormComponentUpdatingBehavior;
import org.apache.wicket.Component;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.markup.html.form.AjaxSubmitLink;
import org.apache.wicket.extensions.markup.html.form.palette.Palette;
import org.apache.wicket.extensions.markup.html.form.palette.component.Recorder;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.IChoiceRenderer;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.LoadableDetachableModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.model.ResourceModel;
import org.apache.wicket.util.string.Strings;

public class AjaxPalettePanel<T extends Serializable> extends AbstractFieldPanel<List<T>> {

    private static final long serialVersionUID = 7738499668258805567L;

    protected Palette<T> palette;

    private final Model<String> queryFilter = new Model<>(StringUtils.EMPTY);

    private final List<T> availableBefore = new ArrayList<>();

    private final LoadableDetachableModel<List<T>> choicesModel;

    public AjaxPalettePanel(
            final String id, final IModel<List<T>> model, final Builder.Query<T> choices, final Builder<T> builder) {
        super(id, builder.name == null ? id : builder.name, model);

        choicesModel = new PaletteLoadableDetachableModel(builder) {

            private static final long serialVersionUID = -108100712154481840L;

            @Override
            protected List<T> getChoices() {
                return choices.execute(getFilter());
            }
        };
        initialize(model, builder);
    }

    public AjaxPalettePanel(
            final String id, final IModel<List<T>> model, final IModel<List<T>> choices, final Builder<T> builder) {
        super(id, builder.name == null ? id : builder.name, model);

        choicesModel = new PaletteLoadableDetachableModel(builder) {

            private static final long serialVersionUID = -108100712154481840L;

            @Override
            protected List<T> getChoices() {
                return builder.filtered
                        ? getFilteredList(choices.getObject(), getFilter().replaceAll("\\*", "\\.\\*"))
                        : choices.getObject();
            }
        };
        initialize(model, builder);
    }

    private void initialize(final IModel<List<T>> model, final Builder<T> builder) {
        setOutputMarkupId(true);

        this.palette = new NonI18nPalette<T>(
                "paletteField", model, choicesModel, builder.renderer, 8, builder.allowOrder, builder.allowMoveAll) {

            private static final long serialVersionUID = -3074655279011678437L;

            @Override
            protected Component newAvailableHeader(final String componentId) {
                return new Label(componentId, new ResourceModel("palette.available", builder.availableLabel));
            }

            @Override
            protected Component newSelectedHeader(final String componentId) {
                return new Label(componentId, new ResourceModel("palette.selected", builder.selectedLabel));
            }

            @Override
            protected Recorder<T> newRecorderComponent() {
                Recorder<T> recorder = new Recorder<T>("recorder", this) {

                    private static final long serialVersionUID = -9169109967480083523L;

                    @Override
                    public List<T> getUnselectedList() {
                        final IChoiceRenderer<? super T> renderer = getPalette().getChoiceRenderer();
                        final Collection<? extends T> choices = getPalette().getChoices();
                        final List<T> unselected = new ArrayList<>(choices.size());
                        final List<String> ids = Arrays.asList(getValue().split(","));

                        for (final T choice : choices) {
                            final String choiceId = renderer.getIdValue(choice, 0);

                            if (!ids.contains(choiceId)) {
                                unselected.add(choice);
                            }
                        }

                        return unselected;
                    }

                    @Override
                    public List<T> getSelectedList() {
                        final IChoiceRenderer<? super T> renderer = getPalette().getChoiceRenderer();
                        final Collection<? extends T> choices = getPalette().getChoices();
                        final List<T> selected = new ArrayList<>(choices.size());

                        // reduce number of method calls by building a lookup table
                        final Map<T, String> idForChoice = new HashMap<>(choices.size());
                        for (final T choice : choices) {
                            idForChoice.put(choice, renderer.getIdValue(choice, 0));
                        }

                        final String value = getValue();
                        int start = value.indexOf(';') + 1;

                        for (final String id : Strings.split(value.substring(start), ',')) {
                            for (final T choice : choices) {
                                final String idValue = idForChoice.get(choice);
                                if (id.equals(idValue)) {
                                    selected.add(choice);
                                    break;
                                }
                            }
                        }

                        return selected;
                    }
                };
                recorder.add(new IndicatorAjaxFormComponentUpdatingBehavior(Constants.ON_CHANGE) {

                    private static final long serialVersionUID = -6139318907146065915L;

                    @Override
                    protected void onUpdate(final AjaxRequestTarget target) {
                        processInput();
                    }
                });

                return recorder;
            }
        };

        add(palette.setLabel(new ResourceModel(name)).setOutputMarkupId(true));

        final Form<?> form = new Form<>("form");
        add(form.setEnabled(builder.filtered).setVisible(builder.filtered));

        final AjaxTextFieldPanel filter = new AjaxTextFieldPanel("filter", "filter", queryFilter, false);
        filter.hideLabel().setOutputMarkupId(true);
        form.add(filter);

        form.add(new AjaxSubmitLink("search") {

            private static final long serialVersionUID = -1765773642975892072L;

            @Override
            protected void onAfterSubmit(final AjaxRequestTarget target, final Form<?> form) {
                super.onAfterSubmit(target, form);
                target.add(palette);
            }
        });
    }

    public LoadableDetachableModel<List<T>> getChoicesModel() {
        return choicesModel;
    }

    @Override
    public AjaxPalettePanel<T> setModelObject(final List<T> object) {
        palette.setDefaultModelObject(object);
        return this;
    }

    public Collection<T> getModelCollection() {
        return palette.getModelCollection();
    }

    public void reload(final AjaxRequestTarget target) {
        target.add(palette);
    }

    @Override
    public AbstractFieldPanel<List<T>> setRequired(final boolean required) {
        palette.setRequired(required);
        return super.setRequired(required);
    }

    public static class Builder<T extends Serializable> implements Serializable {

        private static final long serialVersionUID = 991248996001040352L;

        private IChoiceRenderer<T> renderer;

        private boolean allowOrder;

        private boolean allowMoveAll;

        private String selectedLabel;

        private String availableLabel;

        private boolean filtered;

        private final AjaxPaletteConf conf = new AjaxPaletteConf();

        private String filter = conf.getDefaultFilter();

        private String name;

        public Builder() {
            this.allowMoveAll = false;
            this.allowOrder = false;
            this.filtered = false;
            this.renderer = new SelectChoiceRenderer<>();
        }

        public Builder<T> setName(final String name) {
            this.name = name;
            return this;
        }

        public Builder<T> setAllowOrder(final boolean allowOrder) {
            this.allowOrder = allowOrder;
            return this;
        }

        public Builder<T> setAllowMoveAll(final boolean allowMoveAll) {
            this.allowMoveAll = allowMoveAll;
            return this;
        }

        public Builder<T> setSelectedLabel(final String selectedLabel) {
            this.selectedLabel = selectedLabel;
            return this;
        }

        public Builder<T> setAvailableLabel(final String availableLabel) {
            this.availableLabel = availableLabel;
            return this;
        }

        public Builder<T> setRenderer(final IChoiceRenderer<T> renderer) {
            this.renderer = renderer;
            return this;
        }

        public Builder<T> withFilter() {
            this.filtered = true;
            return this;
        }

        public Builder<T> withFilter(final String defaultFilter) {
            this.filtered = true;
            this.filter = defaultFilter;
            return this;
        }

        public AjaxPalettePanel<T> build(final String id, final IModel<List<T>> model, final IModel<List<T>> choices) {
            return new AjaxPalettePanel<>(id, model, choices, this);
        }

        public AjaxPalettePanel<T> build(final String id, final IModel<List<T>> model, final Query<T> choices) {
            return new AjaxPalettePanel<>(id, model, choices, this);
        }

        public abstract static class Query<T extends Serializable> implements Serializable {

            private static final long serialVersionUID = 3582312993557742858L;

            public abstract List<T> execute(String filter);
        }
    }

    private abstract class PaletteLoadableDetachableModel extends LoadableDetachableModel<List<T>> {

        private static final long serialVersionUID = -7745220313769774616L;

        private final Builder<T> builder;

        PaletteLoadableDetachableModel(final Builder<T> builder) {
            super();
            this.builder = builder;
        }

        protected abstract List<T> getChoices();

        protected String getFilter() {
            return StringUtils.isBlank(queryFilter.getObject()) ? builder.filter : queryFilter.getObject();
        }

        @Override
        protected List<T> load() {
            final List<T> selected = availableBefore.isEmpty()
                    ? new ArrayList<>(palette.getModelCollection())
                    : getSelectedList(availableBefore, palette.getRecorderComponent().getValue());

            availableBefore.clear();
            availableBefore.addAll(ListUtils.sum(selected, getChoices()));
            return availableBefore;
        }

        private List<T> getSelectedList(final Collection<T> choices, final String selection) {
            final IChoiceRenderer<? super T> renderer = palette.getChoiceRenderer();
            final List<T> selected = new ArrayList<>();

            final Map<T, String> idForChoice = new HashMap<>();
            for (final T choice : choices) {
                idForChoice.put(choice, renderer.getIdValue(choice, 0));
            }

            for (final String id : Strings.split(selection, ',')) {
                final Iterator<T> iter = choices.iterator();
                boolean found = false;
                while (!found && iter.hasNext()) {
                    final T choice = iter.next();
                    final String idValue = idForChoice.get(choice);
                    if (id.equals(idValue)) {
                        selected.add(choice);
                        found = true;
                    }
                }
            }

            return selected;
        }

        protected List<T> getFilteredList(final Collection<T> choices, final String filter) {
            final IChoiceRenderer<? super T> renderer = palette.getChoiceRenderer();
            final List<T> selected = new ArrayList<>(choices.size());

            final Map<T, String> idForChoice = new HashMap<>();
            for (final T choice : choices) {
                idForChoice.put(choice, renderer.getIdValue(choice, 0));
            }

            final Pattern pattern = Pattern.compile(filter, Pattern.CASE_INSENSITIVE);

            for (T choice : choices) {
                final String idValue = idForChoice.get(choice);
                if (pattern.matcher(idValue).matches()) {
                    selected.add(choice);
                }
            }

            return selected;
        }
    }
}
