// Copyright 2007, 2008, 2010, 2011, 2012 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.tapestry5.json;

/*
 * Copyright (c) 2002 JSON.org
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 * The Software shall be used for Good, not Evil.
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

/**
 * A JSONArray is an ordered sequence of values. Its external text form is a string wrapped in square brackets with
 * commas separating the values. The internal form is an object having {@code get} and {@code opt} methods for
 * accessing the values by index, and {@code put} methods for adding or replacing values. The values can be any of
 * these types: {@code Boolean}, {@code JSONArray}, {@code JSONObject}, {@code Number},
 * {@code String}, or the {@code JSONObject.NULL object}.
 * <p/>
 * The constructor can convert a JSON text into a Java object. The {@code toString} method converts to JSON text.
 * <p/>
 * A {@code get} method returns a value if one can be found, and throws an exception if one cannot be found. An
 * {@code opt} method returns a default value instead of throwing an exception, and so is useful for obtaining
 * optional values.
 * <p/>
 * The generic {@code get()} and {@code opt()} methods return an object which you can cast or query for type.
 * There are also typed {@code get} and {@code opt} methods that do type checking and type coersion for you.
 * <p/>
 * The texts produced by the {@code toString} methods strictly conform to JSON syntax rules. The constructors are
 * more forgiving in the texts they will accept:
 * <ul>
 * <li>An extra {@code ,}&nbsp;<small>(comma)</small> may appear just before the closing bracket.</li>
 * <li>The {@code null} value will be inserted when there is {@code ,}&nbsp;<small>(comma)</small> elision.</li>
 * <li>Strings may be quoted with {@code '}&nbsp;<small>(single quote)</small>.</li>
 * <li>Strings do not need to be quoted at all if they do not begin with a quote or single quote, and if they do not
 * contain leading or trailing spaces, and if they do not contain any of these characters:
 * {@code { } [ ] / \ : , = ; #} and if they do not look like numbers and if they are not the reserved words
 * {@code true}, {@code false}, or {@code null}.</li>
 * <li>Values can be separated by {@code ;} <small>(semicolon)</small> as well as by {@code ,}
 * <small>(comma)</small>.</li>
 * <li>Numbers may have the {@code 0-} <small>(octal)</small> or {@code 0x-} <small>(hex)</small> prefix.</li>
 * <li>Comments written in the slashshlash, slashstar, and hash conventions will be ignored.</li>
 * </ul>
 *
 * @author JSON.org
 * @version 2
 */
public final class JSONArray extends JSONCollection implements Iterable<Object>
{

    /**
     * The arrayList where the JSONArray's properties are kept.
     */
    private final List<Object> list = new ArrayList<Object>();

    /**
     * Construct an empty JSONArray.
     */
    public JSONArray()
    {
    }

    public JSONArray(String text)
    {
        JSONTokener tokener = new JSONTokener(text);

        parse(tokener);
    }

    public JSONArray(Object... values)
    {
        for (Object value : values)
            put(value);
    }

    /**
     * Create a new array, and adds all values fro the iterable to the array (using {@link #putAll(Iterable)}.
     * <p/>
     * This is implemented as a static method so as not to break the semantics of the existing {@link #JSONArray(Object...)} constructor.
     * Adding a constructor of type Iterable would change the meaning of <code>new JSONArray(new JSONArray())</code>.
     *
     * @param iterable
     *         collection ot value to include, or null
     * @since 5.4
     */
    public static JSONArray from(Iterable<?> iterable)
    {
        return new JSONArray().putAll(iterable);
    }

    @Override
    public Iterator<Object> iterator()
    {
        return list.iterator();
    }

    /**
     * Construct a JSONArray from a JSONTokener.
     *
     * @param tokenizer
     *         A JSONTokener
     * @throws RuntimeException
     *         If there is a syntax error.
     */
    JSONArray(JSONTokener tokenizer)
    {
        assert tokenizer != null;

        parse(tokenizer);
    }

    private void parse(JSONTokener tokenizer)
    {
        if (tokenizer.nextClean() != '[')
        {
            throw tokenizer.syntaxError("A JSONArray text must start with '['");
        }

        if (tokenizer.nextClean() == ']')
        {
            return;
        }

        tokenizer.back();

        while (true)
        {
            if (tokenizer.nextClean() == ',')
            {
                tokenizer.back();
                list.add(JSONObject.NULL);
            } else
            {
                tokenizer.back();
                list.add(tokenizer.nextValue());
            }

            switch (tokenizer.nextClean())
            {
                case ';':
                case ',':
                    if (tokenizer.nextClean() == ']')
                    {
                        return;
                    }
                    tokenizer.back();
                    break;

                case ']':
                    return;

                default:
                    throw tokenizer.syntaxError("Expected a ',' or ']'");
            }
        }
    }

    /**
     * Get the object value associated with an index.
     *
     * @param index
     *         The index must be between 0 and length() - 1.
     * @return An object value.
     * @throws RuntimeException
     *         If there is no value for the index.
     */
    public Object get(int index)
    {
        return list.get(index);
    }

    /**
     * Remove the object associated with the index.
     *
     * @param index
     *         The index must be between 0 and length() - 1.
     * @return An object removed.
     * @throws RuntimeException
     *         If there is no value for the index.
     */
    public Object remove(int index)
    {
        return list.remove(index);
    }

    /**
     * Get the boolean value associated with an index. The string values "true" and "false" are converted to boolean.
     *
     * @param index
     *         The index must be between 0 and length() - 1.
     * @return The truth.
     * @throws RuntimeException
     *         If there is no value for the index or if the value is not convertable to boolean.
     */
    public boolean getBoolean(int index)
    {
        Object value = get(index);

        if (value instanceof Boolean)
        {
            return (Boolean) value;
        }

        if (value instanceof String)
        {
            String asString = (String) value;

            if (asString.equalsIgnoreCase("false"))
                return false;

            if (asString.equalsIgnoreCase("true"))
                return true;
        }

        throw new RuntimeException("JSONArray[" + index + "] is not a Boolean.");
    }

    /**
     * Get the double value associated with an index.
     *
     * @param index
     *         The index must be between 0 and length() - 1.
     * @return The value.
     * @throws IllegalArgumentException
     *         If the key is not found or if the value cannot be converted to a number.
     */
    public double getDouble(int index)
    {
        Object value = get(index);

        try
        {
            if (value instanceof Number)
                return ((Number) value).doubleValue();

            return Double.valueOf((String) value);
        } catch (Exception e)
        {
            throw new IllegalArgumentException("JSONArray[" + index + "] is not a number.");
        }
    }

    /**
     * Get the int value associated with an index.
     *
     * @param index
     *         The index must be between 0 and length() - 1.
     * @return The value.
     * @throws IllegalArgumentException
     *         If the key is not found or if the value cannot be converted to a number. if the
     *         value cannot be converted to a number.
     */
    public int getInt(int index)
    {
        Object o = get(index);
        return o instanceof Number ? ((Number) o).intValue() : (int) getDouble(index);
    }

    /**
     * Get the JSONArray associated with an index.
     *
     * @param index
     *         The index must be between 0 and length() - 1.
     * @return A JSONArray value.
     * @throws RuntimeException
     *         If there is no value for the index. or if the value is not a JSONArray
     */
    public JSONArray getJSONArray(int index)
    {
        Object o = get(index);
        if (o instanceof JSONArray)
        {
            return (JSONArray) o;
        }

        throw new RuntimeException("JSONArray[" + index + "] is not a JSONArray.");
    }

    /**
     * Get the JSONObject associated with an index.
     *
     * @param index
     *         subscript
     * @return A JSONObject value.
     * @throws RuntimeException
     *         If there is no value for the index or if the value is not a JSONObject
     */
    public JSONObject getJSONObject(int index)
    {
        Object o = get(index);
        if (o instanceof JSONObject)
        {
            return (JSONObject) o;
        }

        throw new RuntimeException("JSONArray[" + index + "] is not a JSONObject.");
    }

    /**
     * Get the long value associated with an index.
     *
     * @param index
     *         The index must be between 0 and length() - 1.
     * @return The value.
     * @throws IllegalArgumentException
     *         If the key is not found or if the value cannot be converted to a number.
     */
    public long getLong(int index)
    {
        Object o = get(index);
        return o instanceof Number ? ((Number) o).longValue() : (long) getDouble(index);
    }

    /**
     * Get the string associated with an index.
     *
     * @param index
     *         The index must be between 0 and length() - 1.
     * @return A string value.
     * @throws RuntimeException
     *         If there is no value for the index.
     */
    public String getString(int index)
    {
        return get(index).toString();
    }

    /**
     * Determine if the value is null.
     *
     * @param index
     *         The index must be between 0 and length() - 1.
     * @return true if the value at the index is null, or if there is no value.
     */
    public boolean isNull(int index)
    {
        return get(index) == JSONObject.NULL;
    }

    /**
     * Get the number of elements in the JSONArray, included nulls.
     *
     * @return The length (or size).
     */
    public int length()
    {
        return list.size();
    }

    /**
     * Append an object value. This increases the array's length by one.
     *
     * @param value
     *         An object value. The value should be a Boolean, Double, Integer, JSONArray, JSONObject, JSONLiteral,
     *         Long, or String, or the JSONObject.NULL singleton.
     * @return this array
     */
    public JSONArray put(Object value)
    {
        // now testValidity checks for null values.
        // assert value != null;

        JSONObject.testValidity(value);

        list.add(value);

        return this;
    }

    /**
     * Put or replace an object value in the JSONArray. If the index is greater than the length of the JSONArray, then
     * null elements will be added as necessary to pad it out.
     *
     * @param index
     *         The subscript.
     * @param value
     *         The value to put into the array. The value should be a Boolean, Double, Integer, JSONArray,
     *         JSONObject, JSONString, Long, or String, or the JSONObject.NULL singeton.
     * @return this array
     * @throws RuntimeException
     *         If the index is negative or if the the value is an invalid number.
     */
    public JSONArray put(int index, Object value)
    {
        assert value != null;

        if (index < 0)
        {
            throw new RuntimeException("JSONArray[" + index + "] not found.");
        }

        JSONObject.testValidity(value);

        if (index < length())
        {
            list.set(index, value);
        } else
        {
            while (index != length())
                list.add(JSONObject.NULL);

            list.add(value);
        }

        return this;
    }

    @Override
    public boolean equals(Object obj)
    {
        if (obj == null)
            return false;

        if (!(obj instanceof JSONArray))
            return false;

        JSONArray other = (JSONArray) obj;

        return list.equals(other.list);
    }

    @Override
    void print(JSONPrintSession session)
    {
        session.printSymbol('[');

        session.indent();

        boolean comma = false;

        for (Object value : list)
        {
            if (comma)
                session.printSymbol(',');

            session.newline();

            JSONObject.printValue(session, value);

            comma = true;
        }

        session.outdent();

        if (comma)
            session.newline();

        session.printSymbol(']');
    }

    /**
     * Puts all objects from the collection into this JSONArray, using {@link #put(Object)}.
     *
     * @param collection
     *         List, array, JSONArray, or other iterable object, or null
     * @return this JSONArray
     * @since 5.4
     */
    public JSONArray putAll(Iterable<?> collection)
    {
        if (collection != null)
        {
            for (Object o : collection)
            {
                put(o);
            }
        }

        return this;
    }

    /**
     * Returns an unmodifiable list of the contents of the array. This is a wrapper around the list's internal
     * storage and is live (changes to the JSONArray affect the returned List).
     *
     * @return unmodifiable list of array contents
     * @since 5.4
     */
    public List<Object> toList()
    {
        return Collections.unmodifiableList(list);
    }
}
