/*******************************************************************************
 * Copyright 2015 Univocity Software Pty Ltd
 *
 * 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.univocity.parsers.common.processor.core;

import com.univocity.parsers.common.*;
import com.univocity.parsers.common.processor.*;

import java.util.*;

/**
 * A concrete implementation of {@link Processor} that allows switching among different implementations of
 * {@link Processor} based on values found on the rows parsed from the input.
 */
public abstract class AbstractInputValueSwitch<T extends Context> extends AbstractProcessorSwitch<T> {

	private int columnIndex = -1;
	private NormalizedString columnName = null;
	private Switch[] switches = new Switch[0];
	private Switch defaultSwitch = null;
	private String[] headers;
	private int[] indexes;

	private static final Comparator<String> caseSensitiveComparator = new Comparator<String>() {
		@Override
		public int compare(String o1, String o2) {
			return (o1 == o2 || o1 != null && o1.equals(o2)) ? 0 : 1; //strings are interned, no issues here
		}
	};

	private static final Comparator<String> caseInsensitiveComparator = new Comparator<String>() {
		@Override
		public int compare(String o1, String o2) {
			return (o1 == o2 || o1 != null && o1.equalsIgnoreCase(o2)) ? 0 : 1;  //strings are interned, no issues here
		}
	};

	private Comparator<String> comparator = caseInsensitiveComparator;

	/**
	 * Creates a switch that will analyze the first column of rows found in the input to determine which
	 * {@link Processor} to use for each parsed row
	 */
	public AbstractInputValueSwitch() {
		this(0);
	}

	/**
	 * Creates a switch that will analyze a column of rows parsed from the input to determine which
	 * {@link Processor} to use.
	 *
	 * @param columnIndex the column index whose value will be used to determine which {@link Processor} to use for each parsed row.
	 */
	public AbstractInputValueSwitch(int columnIndex) {
		if (columnIndex < 0) {
			throw new IllegalArgumentException("Column index must be positive");
		}
		this.columnIndex = columnIndex;
	}

	/**
	 * Creates a switch that will analyze a column in rows parsed from the input to determine which
	 * {@link Processor} to use.
	 *
	 * @param columnName name of the column whose values will be used to determine which {@link Processor} to use for each parsed row.
	 */
	public AbstractInputValueSwitch(String columnName) {
		if (columnName == null || columnName.trim().isEmpty()) {
			throw new IllegalArgumentException("Column name cannot be blank");
		}
		this.columnName = NormalizedString.valueOf(columnName);
	}

	/**
	 * Configures the switch to be case sensitive when comparing values provided in {@link #addSwitchForValue(String, Processor, String...)}
	 * with the column given in the constructor of this class.
	 *
	 * @param caseSensitive a flag indicating whether the switch should compare values not considering the character case.
	 */
	public void setCaseSensitive(boolean caseSensitive) {
		this.comparator = caseSensitive ? caseSensitiveComparator : caseInsensitiveComparator;
	}

	/**
	 * Configures the switch to use a custom {@link Comparator} to compare values in the column to analyze which is given in the constructor of this class.
	 *
	 * @param comparator the comparator to use for matching values in the input column with the values provided in  {@link #addSwitchForValue(String, Processor, String...)}
	 */
	public void setComparator(Comparator<String> comparator) {
		if (comparator == null) {
			throw new IllegalArgumentException("Comparator must not be null");
		}
		this.comparator = comparator;
	}

	/**
	 * Defines a default {@link Processor} implementation to use when no matching value is found in the input row.
	 *
	 * @param processor the default processor implementation
	 * @param headersToUse the (optional) sequence of headers to assign to the {@link ParsingContext} of the given processor
	 */
	public void setDefaultSwitch(Processor<T> processor, String... headersToUse) {
		defaultSwitch = new Switch(processor, headersToUse, null, null, null);
	}

	/**
	 * Defines a default {@link Processor} implementation to use when no matching value is found in the input row.
	 *
	 * @param processor the default processor implementation
	 */
	public void setDefaultSwitch(Processor<T> processor) {
		defaultSwitch = new Switch(processor, null, null, null, null);
	}

	/**
	 * Defines a default {@link Processor} implementation to use when no matching value is found in the input row.
	 *
	 * @param processor the default processor implementation
	 * @param indexesToUse the (optional) sequence of column indexes to assign to the {@link ParsingContext} of the given processor
	 */
	public void setDefaultSwitch(Processor<T> processor, int... indexesToUse) {
		defaultSwitch = new Switch(processor, null, indexesToUse, null, null);
	}


	/**
	 * Returns a flag indicating whether this switch contains a default {@link Processor} implementation to use when no matching value is found in the input row.
	 * @return {@code true} if a  {@link Processor} implementation has been provided to process input rows that doesn't have any matching value.
	 */
	public boolean hasDefaultSwitch() {
		return defaultSwitch != null;
	}

	/**
	 * Associates a {@link Processor} implementation with an expected value to be matched in the column provided in the constructor of this class.
	 *
	 * @param value        the value to match against the column of the current input row and trigger the usage of the given processor implementation.
	 * @param processor the processor implementation when the given value matches with the contents in the column provided in the constructor of this class.
	 */
	public void addSwitchForValue(String value, Processor<T> processor) {
		switches = Arrays.copyOf(switches, switches.length + 1);
		switches[switches.length - 1] = new Switch(processor, null, null, value, null);
	}

	/**
	 * Associates a {@link Processor} implementation with an expected value to be matched in the column provided in the constructor of this class.
	 *
	 * @param value        the value to match against the column of the current input row and trigger the usage of the given processor implementation.
	 * @param processor the processor implementation when the given value matches with the contents in the column provided in the constructor of this class.
	 * @param headersToUse the (optional) sequence of headers to assign to the {@link ParsingContext} of the given processor
	 */
	public void addSwitchForValue(String value, Processor<T> processor, String... headersToUse) {
		switches = Arrays.copyOf(switches, switches.length + 1);
		switches[switches.length - 1] = new Switch(processor, headersToUse, null, value, null);
	}


	/**
	 * Associates a {@link Processor} implementation with a custom matching algorithm to be executed in the column provided in the constructor of this class.
	 *
	 * @param matcher      a user defined matching implementation to execute against the values in the column of the current input row and trigger the usage of the given processor implementation.
	 * @param processor the processor implementation when the given value matches with the contents in the column provided in the constructor of this class.
	 */
	public void addSwitchForValue(CustomMatcher matcher, Processor<T> processor) {
		switches = Arrays.copyOf(switches, switches.length + 1);
		switches[switches.length - 1] = new Switch(processor, null, null, null, matcher);
	}

	/**
	 * Associates a {@link Processor} implementation with a custom matching algorithm to be executed in the column provided in the constructor of this class.
	 *
	 * @param matcher      a user defined matching implementation to execute against the values in the column of the current input row and trigger the usage of the given processor implementation.
	 * @param processor the processor implementation when the given value matches with the contents in the column provided in the constructor of this class.
	 * @param headersToUse the (optional) sequence of headers to assign to the {@link ParsingContext} of the given processor
	 */
	public void addSwitchForValue(CustomMatcher matcher, Processor<T> processor, String... headersToUse) {
		switches = Arrays.copyOf(switches, switches.length + 1);
		switches[switches.length - 1] = new Switch(processor, headersToUse, null, null, matcher);
	}

	/**
	 * Associates a {@link Processor} implementation with an expected value to be matched in the column provided in the constructor of this class.
	 *
	 * @param value        the value to match against the column of the current input row and trigger the usage of the given processor implementation.
	 * @param processor the  processor implementation when the given value matches with the contents in the column provided in the constructor of this class.
	 * @param indexesToUse the (optional) sequence of column indexes to assign to the {@link ParsingContext} of the given  processor
	 */
	public void addSwitchForValue(String value, Processor<T> processor, int... indexesToUse) {
		switches = Arrays.copyOf(switches, switches.length + 1);
		switches[switches.length - 1] = new Switch(processor, null, indexesToUse, value, null);
	}

	/**
	 * Associates a {@link Processor} implementation with a custom matching algorithm to be executed in the column provided in the constructor of this class.
	 *
	 * @param matcher      a user defined matching implementation to execute against the values in the column of the current input row and trigger the usage of the given processor implementation.
	 * @param processor the processor implementation when the given value matches with the contents in the column provided in the constructor of this class.
	 * @param indexesToUse the (optional) sequence of column indexes to assign to the {@link ParsingContext} of the given processor
	 */
	public void addSwitchForValue(CustomMatcher matcher, Processor<T> processor, int... indexesToUse) {
		switches = Arrays.copyOf(switches, switches.length + 1);
		switches[switches.length - 1] = new Switch(processor, null, indexesToUse, null, matcher);
	}


	@Override
	public String[] getHeaders() {
		return headers;
	}

	@Override
	public int[] getIndexes() {
		return indexes;
	}

	@Override
	protected final Processor<T> switchRowProcessor(String[] row, T context) {
		if (columnIndex == -1) {
			NormalizedString[] headers = NormalizedString.toIdentifierGroupArray(context.headers());
			if (headers == null) {
				throw new DataProcessingException("Unable to determine position of column named '" + columnName + "' as no headers have been defined nor extracted from the input");
			}
			columnIndex = ArgumentUtils.indexOf(headers, columnName);
			if (columnIndex == -1) {
				throw new DataProcessingException("Unable to determine position of column named '" + columnName + "' as it does not exist in the headers. Available headers are " + Arrays.toString(headers));
			}
		}

		if (columnIndex < row.length) {
			String valueToMatch = row[columnIndex];

			for (int i = 0; i < switches.length; i++) {
				Switch s = switches[i];
				if ((s.matcher != null && s.matcher.matches(valueToMatch)) || comparator.compare(valueToMatch, s.value) == 0) {
					headers = s.headers;
					indexes = s.indexes;
					return s.processor;
				}
			}
		}
		if (defaultSwitch != null) {
			headers = defaultSwitch.headers;
			indexes = defaultSwitch.indexes;
			return defaultSwitch.processor;
		}
		headers = null;
		indexes = null;
		throw new DataProcessingException("Unable to process input row. No switches activated and no default switch defined.", columnIndex, row, null);
	}

	private static class Switch<T extends Context> {
		final Processor<T> processor;
		final String[] headers;
		final int[] indexes;
		final String value;
		final CustomMatcher matcher;

		Switch(Processor<T> processor, String[] headers, int[] indexes, String value, CustomMatcher matcher) {
			this.processor = processor;
			this.headers = headers == null || headers.length == 0 ? null : headers;
			this.indexes = indexes == null || indexes.length == 0 ? null : indexes;
			this.value = value == null ? null : value.intern();
			this.matcher = matcher;
		}

		@Override
		public String toString() {
			return "Switch{" +
					"processor=" + processor +
					", headers=" + Arrays.toString(headers) +
					", indexes=" + Arrays.toString(indexes) +
					", value='" + value + '\'' +
					", matcher=" + matcher +
					'}';
		}
	}
}
