/*
 *  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.isis.core.metamodel.facetapi;

import org.apache.isis.core.commons.ensure.Ensure;
import org.apache.isis.core.commons.matchers.IsisMatchers;

import static org.apache.isis.core.commons.ensure.Ensure.ensureThatArg;
import static org.hamcrest.CoreMatchers.*;

public abstract class FacetAbstract implements Facet {

    public enum Derivation {
        DERIVED,
        NOT_DERIVED
    }
    
    private Facet underlyingFacet;

    private final Class<? extends Facet> facetType;
    private final boolean derived;
    private FacetHolder holder;

    /**
     * Populated in {@link #setFacetHolder(FacetHolder)} if the provided holder
     * implements {@link IdentifiedHolder}.
     * 
     * <p>
     * Otherwise is <tt>null</tt>.
     */
    private IdentifiedHolder identifiedHolder;

    @SuppressWarnings("unchecked")
    public FacetAbstract(
            final Class<? extends Facet> facetType,
            final FacetHolder holder,
            final Derivation derivation) {
        this.facetType = ensureThatArg(facetType, is(not(nullValue(Class.class))));
        setFacetHolder(ensureThatArg(holder, is(not(nullValue(FacetHolder.class)))));
        this.derived = (derivation == Derivation.DERIVED);
    }

    @Override
    public final Class<? extends Facet> facetType() {
        return facetType;
    }

    @Override
    public FacetHolder getFacetHolder() {
        return holder;
    }

    @Override
    public boolean isDerived() {
        return derived;
    }

    /**
     * Convenience method that returns {@link #getFacetHolder()} downcast to
     * {@link IdentifiedHolder} if the implementation does indeed inherit from
     * {@link IdentifiedHolder}, otherwise <tt>null</tt>.
     */
    public IdentifiedHolder getIdentified() {
        return identifiedHolder;
    }

    @Override
    public Facet getUnderlyingFacet() {
        return underlyingFacet;
    }

    @Override
    public void setUnderlyingFacet(final Facet underlyingFacet) {
        if(underlyingFacet != null) {
            if(underlyingFacet instanceof MultiTypedFacet) {
                final MultiTypedFacet multiTypedFacet = (MultiTypedFacet) underlyingFacet;
                final boolean matches = compatible(multiTypedFacet);
                if(!matches) {
                    throw new IllegalArgumentException("illegal argument, expected underlying facet (a multi-valued facet) to have equivalent to the facet type (or facet types) of this facet");
                }
            } else {
                Ensure.ensureThatArg(underlyingFacet.facetType(), IsisMatchers.classEqualTo(facetType));
            }
        }
        this.underlyingFacet = underlyingFacet;
    }

    private boolean compatible(final MultiTypedFacet multiTypedFacet) {

        if (!(this instanceof MultiTypedFacet)) {
            return multiTypedFacet.containsFacetTypeOf(this.facetType);
        }

        final MultiTypedFacet thisAsMultiTyped = (MultiTypedFacet) this;
        final Class<? extends Facet>[] facetTypes = thisAsMultiTyped.facetTypes();
        for (final Class<? extends Facet> facetType : facetTypes) {
            if(multiTypedFacet.containsFacetTypeOf(facetType)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Assume implementation is <i>not</i> a no-op.
     * 
     * <p>
     * No-op implementations should override and return <tt>true</tt>.
     */
    @Override
    public boolean isNoop() {
        return false;
    }

    /**
     * Default implementation of this method that returns <tt>true</tt>, ie
     * should replace (none {@link #isNoop() no-op} implementations.
     * 
     * <p>
     * Implementations that don't wish to replace none no-op implementations
     * should override and return <tt>false</tt>.
     */
    @Override
    public boolean alwaysReplace() {
        return true;
    }

    @Override
    public void setFacetHolder(final FacetHolder facetHolder) {
        this.holder = facetHolder;
        this.identifiedHolder = holder instanceof IdentifiedHolder ? (IdentifiedHolder) holder : null;
    }

    protected String toStringValues() {
        return "";
    }

    @Override
    public String toString() {
        String details = "";
        if (Validating.class.isAssignableFrom(getClass())) {
            details += "Validating";
        }
        if (Disabling.class.isAssignableFrom(getClass())) {
            details += (details.length() > 0 ? ";" : "") + "Disabling";
        }
        if (Hiding.class.isAssignableFrom(getClass())) {
            details += (details.length() > 0 ? ";" : "") + "Hiding";
        }
        if (!"".equals(details)) {
            details = "interaction=" + details + ",";
        }

        final String className = getClass().getName();
        final String stringValues = toStringValues();
        if (getClass() != facetType()) {
            final String facetType = facetType().getName();
            details += "type=" + facetType.substring(facetType.lastIndexOf('.') + 1);
        }
        if (!"".equals(stringValues)) {
            details += ",";
        }
        return className.substring(className.lastIndexOf('.') + 1) + "[" + details + stringValues + "]";
    }

    /**
     * Marker interface used within {@link #toString()}.
     */
    public static interface Hiding {
    }

    /**
     * Marker interface used within {@link #toString()}.
     */
    public static interface Disabling {
    }

    /**
     * Marker interface used within {@link #toString()}.
     */
    public static interface Validating {
    }

}
