/*   Copyright 2004 The Apache Software Foundation
 *
 *   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 org.apache.xmlbeans.soap;

import org.apache.xmlbeans.impl.common.PrefixResolver;
import org.apache.xmlbeans.impl.common.QNameHelper;
import org.apache.xmlbeans.impl.common.XmlWhitespace;
import org.apache.xmlbeans.impl.util.MathUtil;
import org.apache.xmlbeans.impl.values.XmlValueOutOfRangeException;

import javax.xml.namespace.QName;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public final class SOAPArrayType {
    // Example foo:bar[,][][,,][7,9]
    // -> _type = QName(foo:bar)
    // -> _ranks = {2,1,3}
    // -> _dimensions = {7,9}
    private QName _type;
    private int[] _ranks; // if ranks is empty, it means there are no nested arrays
    private int[] _dimensions; // Any dimension can be -1 to indicate "any".

    /**
     * True if the ranks for the passed SOAPArrayType
     * are equal to this one.
     * <p>
     * Does NOT compare the _type fields.
     */
    public boolean isSameRankAs(SOAPArrayType otherType) {
        return Arrays.equals(_ranks, otherType._ranks) &&
               _dimensions.length == otherType._dimensions.length;
    }

    private static int[] internalParseCommaIntString(String csl) {
        List<String> dimStrings = new ArrayList<>();
        int i = 0;
        for (; ; ) {
            int j = csl.indexOf(',', i);
            if (j < 0) {
                dimStrings.add(csl.substring(i));
                break;
            }
            dimStrings.add(csl.substring(i, j));
            i = j + 1;
        }

        return dimStrings.stream().mapToInt(SOAPArrayType::collapseDimString).toArray();
    }

    private static int collapseDimString(String dimString2) {
        String dimString = XmlWhitespace.collapse(dimString2, XmlWhitespace.WS_COLLAPSE);
        try {
            return ("*".equals(dimString) || dimString.isEmpty()) ? -1 : MathUtil.parseAsInt(dimString);
        } catch (Exception e) {
            throw new XmlValueOutOfRangeException("Malformed integer in SOAP array index");
        }
    }

    /**
     * Parses a SOAP 1.1 array type string.
     * <p>
     * Since an array type string contains a QName, a prefix resolver
     * must be passed.
     */
    public SOAPArrayType(String s, PrefixResolver m) {
        int firstbrace = s.indexOf('[');
        if (firstbrace < 0) {
            throw new XmlValueOutOfRangeException();
        }

        // grab the QName
        String firstpart = XmlWhitespace.collapse(s.substring(0, firstbrace), XmlWhitespace.WS_COLLAPSE);
        int firstcolon = firstpart.indexOf(':');
        String prefix = "";
        if (firstcolon >= 0) {
            prefix = firstpart.substring(0, firstcolon);
        }

        String uri = m.getNamespaceForPrefix(prefix);
        if (uri == null) {
            throw new XmlValueOutOfRangeException();
        }

        _type = QNameHelper.forLNS(firstpart.substring(firstcolon + 1), uri);

        initDimensions(s, firstbrace);
    }

    private static final int[] EMPTY_INT_ARRAY = new int[0];

    /**
     * Parses SOAP 1.1(advanced) array type strings.
     * <p>
     * Since in SOAP 1.1(advanced) the dimension specification is separated from the
     * QName for the underlying type, these are passed in separate
     * arguments.
     */
    public SOAPArrayType(QName name, String dimensions) {
        int firstbrace = dimensions.indexOf('[');
        if (firstbrace < 0) {
            _type = name;
            _ranks = EMPTY_INT_ARRAY;
            dimensions = XmlWhitespace.collapse(dimensions, XmlWhitespace.WS_COLLAPSE);
            String[] dimStrings = dimensions.split(" ");
            _dimensions = new int[dimStrings.length];
            for (int i = 0; i < dimStrings.length; i++) {
                String dimString = dimStrings[i];
                if (dimString.equals("*")) {
                    _dimensions[i] = -1;
                    // _hasIndeterminateDimensions = true;
                } else {
                    try {
                        _dimensions[i] = MathUtil.parseAsInt(dimStrings[i]);
                    } catch (Exception e) {
                        throw new XmlValueOutOfRangeException();
                    }
                }
            }
        } else {
            _type = name;
            initDimensions(dimensions, firstbrace);
        }
    }

    /**
     * Given a nested SOAPArrayType and a set of dimensions for the outermost
     * array, comes up with the right SOAPArrayType for the whole thing.
     * <p>
     * E.g.,
     * Nested foo:bar[,][][,,][1,2]
     * Dimensions [6,7,8]
     * Result -&gt; foo:bar[,][][,,][,][6,7,8]
     */
    public SOAPArrayType(SOAPArrayType nested, int[] dimensions) {
        _type = nested._type;

        _ranks = new int[nested._ranks.length + 1];
        System.arraycopy(nested._ranks, 0, _ranks, 0, nested._ranks.length);
        _ranks[_ranks.length - 1] = nested._dimensions.length;

        _dimensions = new int[dimensions.length];
        System.arraycopy(dimensions, 0, _dimensions, 0, dimensions.length);
    }

    /**
     * Initialize dimensions based on SOAP11 parsed dimension substring
     */
    private void initDimensions(String s, int firstbrace) {
        List<String> braces = new ArrayList<>();
        int lastbrace = -1;
        for (int i = firstbrace; i >= 0; ) {
            lastbrace = s.indexOf(']', i);
            if (lastbrace < 0) {
                throw new XmlValueOutOfRangeException();
            }
            braces.add(s.substring(i + 1, lastbrace));
            i = s.indexOf('[', lastbrace);
        }

        String trailer = s.substring(lastbrace + 1);
        if (!XmlWhitespace.isAllSpace(trailer)) {
            throw new XmlValueOutOfRangeException();
        }

        // now fill in rank array
        _ranks = new int[braces.size() - 1];
        for (int i = 0; i < _ranks.length; i++) {
            String commas = braces.get(i);
            int commacount = 0;
            for (int j = 0; j < commas.length(); j++) {
                char ch = commas.charAt(j);
                if (ch == ',') {
                    commacount += 1;
                } else if (!XmlWhitespace.isSpace(ch)) {
                    throw new XmlValueOutOfRangeException();
                }
            }
            _ranks[i] = commacount + 1;
        }

        // finally fill in dimension array
        _dimensions = internalParseCommaIntString(braces.get(braces.size() - 1));

        /*
        for (int i = 0; i < _dimensions.length; i++)
        {
            if (_dimensions[i] < 0)
                _hasIndeterminateDimensions = true;
        }
        */
    }

    /**
     * Returns the QName for the referenced type.
     */
    public QName getQName() {
        return _type;
    }

    /**
     * Returns the array of ranks for inner nested arrays.
     * In SOAP 1.1-advanced, this is always an array of length zero.
     * In SOAP 1.1, this array reflects the ranks of nested
     * arrays. For example foo:bar[,][,,][][5,6] will produce
     * a ranks result of 2, 3, 1.
     */
    public int[] getRanks() {
        int[] result = new int[_ranks.length];
        System.arraycopy(_ranks, 0, result, 0, result.length);
        return result;
    }

    /**
     * Returns the array of dimensions.
     */
    public int[] getDimensions() {
        int[] result = new int[_dimensions.length];
        System.arraycopy(_dimensions, 0, result, 0, result.length);
        return result;
    }

    /**
     * True if this array contains nested arrays. Equivalent
     * to (getRanks().length &gt; 0).
     */
    public boolean containsNestedArrays() {
        return (_ranks.length > 0);
    }

    /**
     * Returns the dimensions as a string, e.g., [,][2,3,4]
     */
    public String soap11DimensionString() {
        return soap11DimensionString(_dimensions);
    }

    /**
     * Given an actual set of dimensions that may differ from
     * the default that is stored, outputs the soap arrayType
     * string.
     */
    public String soap11DimensionString(int[] actualDimensions) {
        StringBuilder sb = new StringBuilder();
        for (int rank : _ranks) {
            sb.append('[');
            for (int j = 1; j < rank; j++) {
                sb.append(',');
            }
            sb.append(']');
        }

        sb.append(IntStream.of(actualDimensions).mapToObj(i -> i >= 0 ? Integer.toString(i) : "").collect(Collectors.joining(",", "[", "]")));
        return sb.toString();
    }

    private SOAPArrayType() {
    }

    /**
     * SOAP 1.2
     * Constructs a SOAPArrayType from soap-enc:itemType and
     * soap-enc:arraySize attributes
     *
     * @param itemType  the item type QName
     * @param arraySize a string with dimentions like: * 3 4
     * @return a SOAPArrayType to represent this
     */
    public static SOAPArrayType newSoap12Array(QName itemType, String arraySize) {
        arraySize = XmlWhitespace.collapse(arraySize, XmlWhitespace.WS_COLLAPSE);
        String[] dimStrings = arraySize.split(" ");
        int[] dimensions = new int[dimStrings.length];
        for (int i = 0; i < dimStrings.length; i++) {
            String dimString = dimStrings[i];
            if (i == 0 && dimString.equals("*")) {
                dimensions[i] = -1;
                // _hasIndeterminateDimensions = true;
            } else {
                try {
                    dimensions[i] = MathUtil.parseAsInt(dimStrings[i]);
                } catch (Exception e) {
                    throw new XmlValueOutOfRangeException();
                }
            }
        }
        SOAPArrayType sot = new SOAPArrayType();
        sot._ranks = EMPTY_INT_ARRAY;
        sot._type = itemType;
        sot._dimensions = dimensions;
        return sot;
    }

    /**
     * SOAP 1.2
     * Given an actual set of dimensions that may differ from
     * the default that is stored, outputs the soap arraySize
     * string.
     */
    public String soap12DimensionString(int[] actualDimensions) {
        return IntStream.of(actualDimensions).
            mapToObj(i -> (i >= 0) ? Integer.toString(i) : "").
            collect(Collectors.joining(" "));
    }

    /**
     * Constructs a SOAPArrayType reflecting the dimensions
     * of the next nested array.
     */
    public SOAPArrayType nestedArrayType() {
        if (!containsNestedArrays()) {
            throw new IllegalStateException();
        }

        SOAPArrayType result = new SOAPArrayType();

        result._type = _type;

        result._ranks = new int[_ranks.length - 1];
        System.arraycopy(_ranks, 0, result._ranks, 0, result._ranks.length);

        result._dimensions = new int[_ranks[_ranks.length - 1]];
        Arrays.fill(result._dimensions, -1);

        // result._hasIndeterminateDimensions = (result._dimensions.length > 0);

        return result;
    }

    public int hashCode() {
        return (_type.hashCode() + _dimensions.length + _ranks.length + (_dimensions.length == 0 ? 0 : _dimensions[0]));
    }

    public boolean equals(Object obj) {
        if (obj == this) {
            return true;
        }

        if (!(obj instanceof SOAPArrayType)) {
            return false;
        }

        SOAPArrayType sat = (SOAPArrayType) obj;

        return _type.equals(sat._type) &&
               Arrays.equals(_ranks, sat._ranks) &&
               Arrays.equals(_dimensions, sat._dimensions);
    }
}
