/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * 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 com.android.apksigner;

import java.util.Arrays;

/**
 * Parser of command-line options/switches/flags.
 *
 * <p>Supported option formats:
 * <ul>
 * <li>{@code --name value}</li>
 * <li>{@code --name=value}</li>
 * <li>{@code -name value}</li>
 * <li>{@code --name} (boolean options only)</li>
 * </ul>
 *
 * <p>To use the parser, create an instance, providing it with the command-line parameters, then
 * iterate over options by invoking {@link #nextOption()} until it returns {@code null}.
 */
class OptionsParser {
    private final String[] mParams;
    private int mIndex;
    private int mPutBackIndex;
    private String mLastOptionValue;
    private String mPutBackLastOptionValue;
    private String mLastOptionOriginalForm;
    private String mPutBackLastOptionOriginalForm;

    /**
     * Constructs a new {@code OptionsParser} initialized with the provided command-line.
     */
    public OptionsParser(String[] params) {
        mParams = params.clone();
    }

    /**
     * Returns the name (without leading dashes) of the next option (starting with the very first
     * option) or {@code null} if there are no options left.
     *
     * <p>The value of this option can be obtained via {@link #getRequiredValue(String)},
     * {@link #getRequiredIntValue(String)}, and {@link #getOptionalBooleanValue(boolean)}.
     */
    public String nextOption() {
        if (mIndex >= mParams.length) {
            // No more parameters left
            return null;
        }
        String param = mParams[mIndex];
        if (!param.startsWith("-")) {
            // Not an option
            return null;
        }

        mPutBackIndex = mIndex;
        mIndex++;
        mPutBackLastOptionOriginalForm = mLastOptionOriginalForm;
        mLastOptionOriginalForm = param;
        mPutBackLastOptionValue = mLastOptionValue;
        mLastOptionValue = null;
        if (param.startsWith("--")) {
            // FORMAT: --name value OR --name=value
            if ("--".equals(param)) {
                // End of options marker
                return null;
            }
            int valueDelimiterIndex = param.indexOf('=');
            if (valueDelimiterIndex != -1) {
                mLastOptionValue = param.substring(valueDelimiterIndex + 1);
                mLastOptionOriginalForm = param.substring(0, valueDelimiterIndex);
                return param.substring("--".length(), valueDelimiterIndex);
            } else {
                return param.substring("--".length());
            }
        } else {
            // FORMAT: -name value
            return param.substring("-".length());
        }
    }

    /**
     * Undoes the last call to nextOption(), if one was made.  This allows callers to unwind state
     * so as not to eat up an option that is meant to be processed elsewhere.
     */
    public void putOption() {
        mIndex = mPutBackIndex;
        mLastOptionOriginalForm = mPutBackLastOptionOriginalForm;
        mLastOptionValue = mPutBackLastOptionValue;
    }

    /**
     * Returns the original form of the current option. The original form includes the leading dash
     * or dashes. This is intended to be used for referencing the option in error messages.
     */
    public String getOptionOriginalForm() {
        return mLastOptionOriginalForm;
    }

    /**
     * Returns the value of the current option, throwing an exception if the value is missing.
     */
    public String getRequiredValue(String valueDescription) throws OptionsException {
        if (mLastOptionValue != null) {
            String result = mLastOptionValue;
            mLastOptionValue = null;
            return result;
        }
        if (mIndex >= mParams.length) {
            // No more parameters left
            throw new OptionsException(
                    valueDescription + " missing after " + mLastOptionOriginalForm);
        }
        String param = mParams[mIndex];
        if ("--".equals(param)) {
            // End of options marker
            throw new OptionsException(
                    valueDescription + " missing after " + mLastOptionOriginalForm);
        }
        mIndex++;
        return param;
    }

    /**
     * Returns the value of the current numeric option, throwing an exception if the value is
     * missing or is not numeric.
     */
    public int getRequiredIntValue(String valueDescription) throws OptionsException {
        String value = getRequiredValue(valueDescription);
        try {
            return Integer.parseInt(value);
        } catch (NumberFormatException e) {
            throw new OptionsException(
                    valueDescription + " (" + mLastOptionOriginalForm
                            + ") must be a decimal number: " + value);
        }
    }

    /**
     * Gets the value of the current boolean option. Boolean options are not required to have
     * explicitly specified values.
     */
    public boolean getOptionalBooleanValue(boolean defaultValue) throws OptionsException {
        if (mLastOptionValue != null) {
            // --option=value form
            String stringValue = mLastOptionValue;
            mLastOptionValue = null;
            if ("true".equals(stringValue)) {
                return true;
            } else if ("false".equals(stringValue)) {
                return false;
            }
            throw new OptionsException(
                    "Unsupported value for " + mLastOptionOriginalForm + ": " + stringValue
                            + ". Only true or false supported.");
        }

        // --option (true|false) form OR just --option
        if (mIndex >= mParams.length) {
            return defaultValue;
        }

        String stringValue = mParams[mIndex];
        if ("true".equals(stringValue)) {
            mIndex++;
            return true;
        } else if ("false".equals(stringValue)) {
            mIndex++;
            return false;
        } else {
            return defaultValue;
        }
    }

    /**
     * Returns the remaining command-line parameters. This is intended to be invoked once
     * {@link #nextOption()} returns {@code null}.
     */
    public String[] getRemainingParams() {
        if (mIndex >= mParams.length) {
            return new String[0];
        }
        String param = mParams[mIndex];
        if ("--".equals(param)) {
            // Skip end of options marker
            return Arrays.copyOfRange(mParams, mIndex + 1, mParams.length);
        } else {
            return Arrays.copyOfRange(mParams, mIndex, mParams.length);
        }
    }

    /**
     * Indicates that an error was encountered while parsing command-line options.
     */
    public static class OptionsException extends Exception {
        private static final long serialVersionUID = 1L;

        public OptionsException(String message) {
            super(message);
        }
    }
}
