/*
 * Copyright (c) 2016 Villu Ruusmann
 *
 * This file is part of JPMML-R
 *
 * JPMML-R is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * JPMML-R is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with JPMML-R.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.jpmml.rexp;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Set;

import org.dmg.pmml.Expression;
import org.dmg.pmml.FieldName;
import org.dmg.pmml.PMMLObject;
import org.dmg.pmml.Visitor;
import org.dmg.pmml.VisitorAction;
import org.jpmml.model.visitors.FieldReferenceFinder;

public class FunctionExpression extends Expression {

	private String namespace = null;

	private String function = null;

	private List<Argument> arguments = null;


	public FunctionExpression(String function, List<Argument> arguments){
		this(null, function, arguments);
	}

	public FunctionExpression(String namespace, String function, List<Argument> arguments){
		this.namespace = namespace;
		this.function = function;
		this.arguments = arguments;
	}

	public boolean hasId(String namespace, String function){
		return (Objects.equals(this.namespace, namespace) || Objects.equals(this.namespace, null)) && Objects.equals(this.function, function);
	}

	public Argument getArgument(String tag, int index){

		if(tag != null){

			try {
				return getArgument(tag);
			} catch(IllegalArgumentException iae){
				// Ignored
			}
		}

		return getArgument(index);
	}

	public Argument getArgument(String tag){

		if(tag == null){
			throw new NullPointerException();
		} // End if

		if(this.arguments == null){
			throw new IllegalArgumentException(tag);
		}

		List<Argument> arguments = this.arguments;
		for(Argument argument : arguments){

			if((tag).equals(argument.getTag())){
				return argument;
			}
		}

		throw new IllegalArgumentException(tag);
	}

	public Argument getArgument(int index){

		if(this.arguments == null){
			throw new ArrayIndexOutOfBoundsException(index);
		}

		return this.arguments.get(index);
	}

	public String getNamespace(){
		return this.namespace;
	}

	public FunctionExpression setNamespace(String namespace){
		this.namespace = namespace;

		return this;
	}

	public String getFunction(){
		return this.function;
	}

	public FunctionExpression setFunction(String function){
		this.function = function;

		return this;
	}

	public List<Argument> getArguments(){

		if(this.arguments == null){
			this.arguments = new ArrayList<>();
		}

		return this.arguments;
	}

	public boolean hasArguments(){
		return (this.arguments != null && this.arguments.size() > 0);
	}

	public FunctionExpression addArguments(Argument... arguments){
		getArguments().addAll(Arrays.asList(arguments));

		return this;
	}

	@Override
	public VisitorAction accept(Visitor visitor){
		VisitorAction status = visitor.visit(this);

		if(status == VisitorAction.CONTINUE){
			visitor.pushParent(this);

			if(hasArguments()){
				List<Argument> arguments = getArguments();

				for(Argument argument : arguments){
					status = PMMLObject.traverse(visitor, argument.getExpression());

					if(status != VisitorAction.CONTINUE){
						break;
					}
				}
			}

			visitor.popParent();
		} // End if

		if(status == VisitorAction.TERMINATE){
			return VisitorAction.TERMINATE;
		}

		return VisitorAction.CONTINUE;
	}

	static
	public class Argument {

		private Token begin = null;

		private Token end = null;

		private String tag = null;

		private Expression expression = null;


		public Argument(Expression expression){
			this(null, expression);
		}

		public Argument(String tag, Expression expression){
			setTag(tag);
			setExpression(expression);
		}

		public String format(){
			Token begin = getBegin();
			Token end = getEnd();

			if(begin == null || end == null){
				throw new IllegalStateException();
			}

			return format(begin, end);
		}

		public String formatExpression(){
			Token begin = getBegin();
			Token end = getEnd();

			if(begin == null || end == null){
				throw new IllegalStateException();
			}

			switch(begin.next.kind){
				case ExpressionTranslatorConstants.IDENTIFIER:
				case ExpressionTranslatorConstants.STRING:

					switch(begin.next.next.kind){
						case ExpressionTranslatorConstants.ASSIGN:
							begin = begin.next.next;
							break;
						default:
							break;
					}
					break;
				default:
					break;
			}

			return format(begin, end);
		}

		public Set<FieldName> getFieldNames(){
			Expression expression = getExpression();

			FieldReferenceFinder fieldReferenceFinder = new FieldReferenceFinder();
			fieldReferenceFinder.applyTo(expression);

			return fieldReferenceFinder.getFieldNames();
		}

		Token getBegin(){
			return this.begin;
		}

		void setBegin(Token begin){
			this.begin = begin;
		}

		Token getEnd(){
			return this.end;
		}

		void setEnd(Token end){
			this.end = end;
		}

		public String getTag(){
			return this.tag;
		}

		private void setTag(String tag){
			this.tag = tag;
		}

		public boolean hasTag(){
			return (this.tag != null);
		}

		public Expression getExpression(){
			return this.expression;
		}

		private void setExpression(Expression expression){
			this.expression = expression;
		}

		static
		private String format(Token begin, Token end){
			StringBuilder sb = new StringBuilder();

			for(Token token = begin.next; token != end; token = token.next){
				int pos = sb.length();

				if(token != begin.next){

					for(Token specialToken = token.specialToken; specialToken != null; specialToken = specialToken.specialToken){
						sb.insert(pos, specialToken.image);
					}
				}

				sb.append(token.image);
			}

			return sb.toString();
		}
	}
}