/*
 * 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.openjpa.kernel;

import java.io.Serializable;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * Describes the shape of a query result.
 * <br>
 * A shape is described as a Java class by the generic type argument T. A shape may contain zero or more shapes.
 * A shape is categorized as follows:
 * <LI>A <em>primitive</em> shape can not have child shapes e.g. Foo or float.
 * <LI>A <em>compound</em> shape has zero or more child shapes e.g. Foo{} or Foo{String, int} or
 * Foo{String,Bar{Double},int}.
 * <LI>A <em>nesting</em> shape has one or more compound child shape(s).
 * For example,  Foo{String,Bar{Double},int}. On the other hand, Foo{String, int} is a compound shape but is not
 * nesting because all its child shapes are primitive.
 * <br>
 * A primitive category shape is declared during construction and immutable.
 * The category of a non-primitive shape is mutable.
 * <br>
 * Notice that all nested shapes are compound shapes but not all compound shapes are nesting.
 * <br>
 * A compound shape can <em>add</em> other primitive shapes or <em>nest</em> other shapes to any arbitrary depth.
 * However, a shape does not allow recursive nesting of shapes.
 * <br>
 * <B>Usage</B>:
 * The purpose of a shape is to populate an instance of T from an array of input values where each
 * array element is further specified with a type and an alias. FillStrategy determines how a shape
 * populates an instance of T by consuming the input array element values.
 * The input data is presented as an Object[] with a parallel array of types because the primitive
 * types (short, float etc.) are not preserved in the input array. For certain FillStrategy such as
 * MAP or BEAN, the alias of the input array element are used to identify the Map key or setter
 * methods respectively.
 *
 * @author Pinaki Poddar
 *
 * @since 2.0.0
 *
 */
public class ResultShape<T> implements Serializable {
    private static final long serialVersionUID = 1L;
    private final Class<T> cls;        // the type of value this shape represents or populates
    private final boolean isPrimitive; // flags this shape as primitive
    private boolean isNesting;         // flags this shape as nesting
    private String alias;

    private final FillStrategy<T> strategy;   // the strategy to populate this shape
    private final List<ResultShape<?>> children; // children of this shape. null for primitives
    private Set<ResultShape<?>> parents;   // the shapes that have nested this shape

    /**
     * Construct a non-primitive shape with ASSIGN or ARRAY fill strategy.
     */
    public ResultShape(Class<T> cls) {
        this(cls, false);
    }

    /**
     * Construct a primitive or non-primitive shape with ASSIGN or ARRAY fill strategy.
     * If the shape is declared as primitive then the given class can not be an array.
     */
    public ResultShape(Class<T> cls, boolean primitive) {
        this(cls, cls.isArray() ? new FillStrategy.Array<>(cls) : new FillStrategy.Assign<>(), primitive);
        if (cls.isArray() && primitive)
            throw new IllegalArgumentException(cls.getSimpleName() + " can not be primitive shape");
    }

    /**
     *
     * Construct a non-primitive shape with the given fill strategy.
     */
    public ResultShape(Class<T> cls, FillStrategy<T> strategy) {
        this(cls, strategy, false);
    }

    /**
     * Construct a shape with the given fill strategy.
     *
     */
    public ResultShape(Class<T> cls, FillStrategy<T> strategy, boolean primitive) {
        if (cls == null) throw new NullPointerException();
        this.cls = cls;
        this.strategy = strategy;
        isPrimitive = primitive;
        children = isPrimitive ? null : new ArrayList<>();
    }

//    /**
//     * Construct a shape with the MAP fill strategy to invoke the given method.
//     *
//     */
//    public ResultShape(Class<T> cls, Method putMethod) {
//        if (cls == null) throw new NullPointerException();
//        this.cls = cls;
//        this.strategy = new FillStrategy.Map<T>(putMethod);
//        isPrimitive = true;
//        children = new ArrayList<ResultShape<?>>();
//    }
//
//    /**
//     * Construct a shape with the CONSTRUCTOR fill strategy to invoke the given constructor.
//     *
//     */
//    public ResultShape(Class<T> cls, Constructor<? extends T> cons) {
//        if (cls == null) throw new NullPointerException();
//        this.cls = cls;
//        this.strategy = new FillStrategy.NewInstance<T>(cons);
//        isPrimitive = false;
//        children = new ArrayList<ResultShape<?>>();
//    }

    /**
     * Gets the type of instance populated by this shape.
     */
    public Class<T> getType() {
        return cls;
    }

    public FillStrategy<T> getStrategy() {
        return strategy;
    }

    public ResultShape<T> setAlias(String alias) {
        this.alias = alias;
        return this;
    }

    public String getAlias() {
        return alias;
    }

    /**
     * Gets the list of classes to compose this shape and all its children.
     * For example, a shape Foo{String,Bar{int, Date}, Double} will return
     * {String, int, Date, Double}
     */
    public List<Class<?>> getCompositeTypes() {
        List<Class<?>> result = new ArrayList<>();
        if (isPrimitive() || children.isEmpty()) {
            result.add(cls);
        } else {
            for (ResultShape<?> child : children) {
                result.addAll(child.getCompositeTypes());
            }
        }
        return result;
    }

    /**
     * Gets the list of classes to compose this shape only i.e. without expanding the children's shape.
     * For example, a shape Foo{String,Bar{int, Date}, Double} will return {String, Bar, Double}
     */
    public List<Class<?>> getTypes() {
        List<Class<?>> result = new ArrayList<>();
        if (children.isEmpty()) {
            result.add(cls);
        } else {
            for (ResultShape<?> child : children) {
                result.add(child.getType());
            }
        }
        return result;
    }

    /**
     * Creates a new shape of type X with the given class arguments and nests
     * the new shape within this shape.
     *
     * @return newly created nested shape
     */
    public <X> ResultShape<X> nest(Class<X> cls, FillStrategy strategy, Class<?>... classes) {
        assertNotPrimitive();
        ResultShape<X> child = new ResultShape<X>(cls, strategy, true);
        this.nest(child.add(classes));
        return child;
    }

    /**
     * Nest the given shape.
     *
     * @param shape The given shape can not be a parent of this shape
     * to prohibit recursive nesting.
     *
     * @return this shape itself
     */
    public ResultShape<T> nest(ResultShape<?> shape) {
        assertNotPrimitive();
        if (shape.isParent(this))
            throw new IllegalArgumentException(this + " can not nest recursive " + shape);
        children.add(shape);
        shape.addParent(this);
        isNesting |= !shape.isPrimitive();
        return this;
    }

    /**
     * Adds the given shape as one of the parents of this shape.
     *
     */
    private void addParent(ResultShape<?> p) {
        if (parents == null)
            parents = new HashSet<>();
        parents.add(p);
    }

    /**
     * Adds the given classes as child shapes of this shape.
     * The child shapes are primitive shapes.
     */
    public ResultShape<T> add(Class<?>... classes) {
        assertNotPrimitive();
        for (Class<?> c : classes) {
            children.add(new ResultShape(c, true));
        }
        return this;
    }

    /**
     * Gets all the child shapes.
     */
    public List<ResultShape<?>> getChildren() {
        return Collections.unmodifiableList(children);
    }

    /**
     * Affirms if this shape can have child shapes.
     */
    public boolean isCompound() {
        return !isPrimitive;
    }

    /**
     * Affirms if this shape can not have any child shape.
     * A primitive shape uses ASSIGN strategy.
     */
    public boolean isPrimitive() {
        return isPrimitive;
    }

    /**
     * Affirms if at least one child shape of this shape is a compound shape.
     */
    public boolean isNesting() {
        return isNesting;
    }

    /**
     * Affirms if this shape is nested within other shapes.
     */
    public boolean isNested() {
        return parents != null;
    }

    /**
     * Affirms if the given shape is a parent (or grandparent) of this shape.
     */
    public boolean isParent(ResultShape<?> p) {
        if (p.getParents().contains(this))
            return true;
        if (children != null) {
            for (ResultShape<?> child : children) {
                if (child.isParent(p))
                    return true;
            }
        }
        return false;
    }

    void assertNotPrimitive() {
        if (isPrimitive)
            throw new UnsupportedOperationException("Can not add/nest shape to primitive shape " + this);
    }

    /**
     * Gets the immediate parents of this shape.
     */
    public Set<ResultShape<?>> getParents() {
        return parents == null ? Collections.EMPTY_SET : Collections.unmodifiableSet(parents);
    }

    /**
     * Total number of arguments required to populate the shape and all its child shapes.
     */
    public int argLength() {
        if (isPrimitive() || children.isEmpty())
            return 1;
        int l = 0;
        for (ResultShape<?> child : children) {
            l += child.argLength();
        }
        return l;
    }

    /**
     * Number of arguments to populate this shape only.
     */
    public int length() {
        if (isPrimitive() || children.isEmpty())
            return 1;
        return children.size();
    }

    // ======================================================================================
    // Data Population Routines
    // ======================================================================================

    /**
     * Fill this shape and its children with the given array element values.
     * The parallel arrays contain the actual values, the types of these values and aliases.
     * The type and alias information are used for packing Map or invoking constructor.
     * The type can be different from what can be determined from array elements because
     * of boxing of primitive types.
     * The actual constructor argument types are sourced from types[] array.
     */
    public T pack(Object[] values, Class<?>[] types, String[] aliases) {
        if (values.length < argLength()) // input can be longer than required
            throw new IndexOutOfBoundsException(values.length + " values are less than " +
                    argLength() + " argumenets required to pack " + this);
        Object[] args = new Object[length()];
        Class<?>[] argTypes = new Class[length()];
        String[] argAliases = new String[length()];
        if (isPrimitive() || children.isEmpty()) {
            args[0] = values[0];
            argTypes[0] = types[0];
            argAliases[0] = aliases[0];
        } else { // pack each children
            int start = 0;
            int i = 0;
            for (ResultShape<?> rs : children) {
                int finish = start + rs.argLength();
                args[i] = rs.pack(chop(values, start, finish), chop(types, start, finish),
                        chop(aliases, start, finish));
                argTypes[i] = rs.getType();
                argAliases[0] = rs.getAlias();
                start = finish;
                i++;
            }
        }
        return strategy.fill(args, argTypes, argAliases);
    }

    /**
     * Chop an array from start to finish.
     */
    <X> X[] chop(X[] values, int start, int finish) {
        X[] result = (X[])Array.newInstance(values.getClass().getComponentType(), finish-start);
        System.arraycopy(values, start, result, 0, finish-start);
        return result;
    }
    /**
     * Gets a human-readable representation of this shape.
     *
     */
    @Override
    public String toString() {
        StringBuilder buf = new StringBuilder(cls.getSimpleName());
        if (isPrimitive() || children.isEmpty())
            return buf.toString();
        int i = 0;
        for (ResultShape<?> child : children) {
            buf.append(i++ == 0 ? "{" : ", ");
            buf.append(child);
        }
        if (!children.isEmpty())
            buf.append("}");
        return buf.toString();
    }
}
