/*******************************************************************************
 * Copyright (c) 2016 Attila Kovacs <attila[AT]sigmyne.com>.
 * All rights reserved. 
 * 
 * This file is part of crush.
 * 
 *     crush is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 * 
 *     crush 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 General Public License for more details.
 * 
 *     You should have received a copy of the GNU General Public License
 *     along with crush.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * Contributors:
 *     Attila Kovacs <attila[AT]sigmyne.com> - initial API and implementation
 ******************************************************************************/
package crush.instrument;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Hashtable;

import jnum.Configurator;
import jnum.Unit;
import jnum.data.fitting.Parameter;
import jnum.math.Vector2D;



public class DistortionModel extends Hashtable<DistortionModel.Term, Parameter> {
	/**
	 * 
	 */
	private static final long serialVersionUID = 2378434929629074050L;
	private String id = "[n/a]";
	private Unit unit = Unit.unity;
	
	public DistortionModel() {}
	
	public DistortionModel(Configurator options) {
		this();
		setOptions(options);
	}
	
	public void setOptions(Configurator options) {
		clear();
		
		for(String key : options.branches.keySet()) {
			if(key.length() != 3) continue;
			int dir = key.charAt(0);
			int xExp = key.charAt(1) - '0';
			int yExp = key.charAt(2) - '0';
			if(xExp < 0 || yExp < 0) continue;
			double value = options.option(key).getDouble();
			if(dir == 'x') setX(xExp, yExp, value);
			else if(dir == 'y') setY(xExp, yExp, value);	
		}
		
		if(options.containsKey("unit")) unit = Unit.get(options.option("unit").getValue());
		if(options.containsKey("name")) id = options.option("name").getValue();
	}
	
	public String getName() { return id; }
	
	public Unit getUnit() { return unit; }
	
	public void setUnit(Unit u) { this.unit = u; }
	
	public void scale(double x) {
		for(Term term : keySet()) get(term).scale(x);
	}
	
	public void set(int xExp, int yExp, double cx, double cy) {
		setX(xExp, yExp, cx);
		setY(xExp, yExp, cy);
	}
	
	public Term getTerm(String dir, int xExp, int yExp) {
	    Term match = new Term(dir, xExp, yExp);
        for(Term term : keySet()) if(term.equals(match)) return term;
        Parameter p = match.createParameter(0.0);
        put(match, p);
        return match;
	}
	
	public Parameter getParameter(String dir, int xExp, int yExp) {
	    return get(getTerm(dir, xExp, yExp));
	}
	
	public void setX(int xExp, int yExp, double value) {
		Term term = new Term("x", xExp, yExp);
		if(containsKey(term)) get(term).setValue(value);
		else put(term, term.createParameter(value));		
	}
	
	public void setY(int xExp, int yExp, double value) {
		Term term = new Term("y", xExp, yExp);
		if(containsKey(term)) get(term).setValue(value);
		else put(term, term.createParameter(value));		
	}
	
	public void distort(Vector2D v) { v.add(getValue(v)); }
	
	public Vector2D getValue(Vector2D v) {
		return getValue(v.x(), v.y());
	}
	
	public Vector2D getValue(double x, double y) {
		Vector2D sum = new Vector2D();
		for(Term term : keySet()) {
		    double termValue = get(term).value() * term.getValue(x / unit.value(), y / unit.value());
		    
		    if(term.dir == "x") sum.addX(termValue);
		    else if(term.dir == "y") sum.addY(termValue);
		}
		sum.scale(unit.value());
		return sum;	
	}

	@Override
	public String toString() {
		ArrayList<Term> sorted = new ArrayList<>(keySet());
		Collections.sort(sorted);
		
		StringBuffer buf = new StringBuffer();
		
		for(Term term : sorted) {
			buf.append("  ");
			buf.append(term);
			buf.append(": ");
			buf.append(get(term));
			buf.append("\n");
		}
		
		return "Distortion:\n" + new String(buf);
	}
	
	public class Term implements Comparable<Term>, Serializable {
		/**
         * 
         */
        private static final long serialVersionUID = 8495738757424333815L;
        String dir;
        int xExp, yExp;
		
		public Term(String dir, int expX, int expY) {
		    this.dir = dir; this.xExp = expX; this.yExp = expY; 
		}
		
		public double getValue(double x, double y) {
			return Math.pow(x, xExp) * Math.pow(y, yExp);			
		}

		@Override
		public int compareTo(Term arg0) {
			if(equals(arg0)) return 0;
			if(xExp < arg0.xExp) return -1;
			if(xExp > arg0.xExp) return 1;
			if(yExp < arg0.yExp) return -1;
			return dir.compareTo(arg0.dir);
		}
		
		@Override
		public String toString() {
			return dir + "[" + xExp + "," + yExp + "]";
		}
		
		public double getStepSize() {
		    return Math.pow(0.01, 0.5*(xExp+yExp+1));
		}
		
		public Parameter createParameter(double value) {
		    return new Parameter(toString(), value, getStepSize());
		}
	}
	
}
