/*
 * Copyright 2009 Yuichiro Moriguchi
 *
 * 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 net.morilib.lisp;

import java.io.IOException;
import java.io.PushbackReader;
import java.io.Reader;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.List;
import java.util.logging.Logger;

import net.morilib.lisp.util.ReflContainer;
import net.morilib.util.io.CharUnreadBuffer;

/**
 * 
 *
 *
 * @author MORIGUCHI, Yuichiro 2009
 */
/*pacakge*/ final class IntLispUtils {

	//
	private IntLispUtils() {
		// do nothing
	}

	/*package*/ static Datum car(Datum d, LispMessage mesg) {
		if(d instanceof Cons) {
			return ((Cons)d).getCar();
		}
		throw mesg.getError("err.require.pair"); 
	}

	/*package*/ static Datum cdr(Datum d, LispMessage mesg) {
		if(d instanceof Cons) {
			return ((Cons)d).getCdr();
		}
		throw mesg.getError("err.require.pair"); 
	}

	/*package*/ static Datum caar(Datum d, LispMessage mesg) {
		if(d instanceof Cons) {
			Datum c0 = ((Cons)d).getCar();

			if(c0 instanceof Cons) {
				return ((Cons)c0).getCar();
			}
		}
		throw mesg.getError("err.require.pair"); 
	}

	/*package*/ static Datum cdar(Datum d, LispMessage mesg) {
		if(d instanceof Cons) {
			Datum c0 = ((Cons)d).getCar();

			if(c0 instanceof Cons) {
				return ((Cons)c0).getCdr();
			}
		}
		throw mesg.getError("err.require.pair"); 
	}

	/*package*/ static Datum cadr(Datum d, LispMessage mesg) {
		if(d instanceof Cons) {
			Datum c0 = ((Cons)d).getCdr();

			if(c0 instanceof Cons) {
				return ((Cons)c0).getCar();
			}
		}
		throw mesg.getError("err.require.pair"); 
	}

	/*package*/ static Datum cddr(Datum d, LispMessage mesg) {
		if(d instanceof Cons) {
			Datum c0 = ((Cons)d).getCdr();

			if(c0 instanceof Cons) {
				return ((Cons)c0).getCdr();
			}
		}
		throw mesg.getError("err.require.pair"); 
	}

	/*package*/ /*static Cons extractLambda(
			Datum bcdr, Datum bcar) {
		if(bcdr instanceof Cons) {
			Cons  c0 = (Cons)bcdr;
			Datum d0 = c0.getCar();

			if(d0 instanceof Cons &&
					((Cons)d0).getCar().equals(LAMBDA)) {
				Cons c1 = (Cons)d0;

				if(c1.getCdr() instanceof Cons) {
					Cons c2 = (Cons)c1.getCdr();

					// error check
					return new Cons(bcar, c2.getCar());
				}
			}
		}
		return null;
	}*/

	/*package*/ /*static Datum extractLambdaList(Datum bcdr) {
		if(bcdr instanceof Cons) {
			Cons  c0 = (Cons)bcdr;
			Datum d0 = c0.getCar();

			if(d0 instanceof Cons &&
					((Cons)d0).getCar().equals(LAMBDA)) {
				Cons c1 = (Cons)d0;

				if(c1.getCdr() instanceof Cons) {
					Cons c2 = (Cons)c1.getCdr();

					// error check
					return c2.getCdr();
				}
			}
		}
		return null;
	}*/


	/*package*/ static boolean isAssignableInt(Class<?> rtp) {
		return (Integer.TYPE.isAssignableFrom(rtp) ||
				Integer.class.isAssignableFrom(rtp));
	}

	/*package*/ static boolean isAssignableLong(Class<?> rtp) {
		return (Long.TYPE.isAssignableFrom(rtp) ||
				Long.class.isAssignableFrom(rtp));
	}

	/*package*/ static boolean isAssignableFloat(Class<?> rtp) {
		return (Float.TYPE.isAssignableFrom(rtp) ||
				Float.class.isAssignableFrom(rtp));
	}

	/*package*/ static boolean isAssignableDouble(Class<?> rtp) {
		return (Double.TYPE.isAssignableFrom(rtp) ||
				Double.class.isAssignableFrom(rtp));
	}

	/*package*/ static boolean isAssignableBigInteger(Class<?> rtp) {
		return (BigInteger.class.isAssignableFrom(rtp));
	}

	/*package*/ static boolean isAssignableBigDecimal(Class<?> rtp) {
		return (BigDecimal.class.isAssignableFrom(rtp));
	}

	/*package*/ static BigInteger toIntegerExact(double number) {
		try {
			BigDecimal dec;

			if(Double.isInfinite(number) || Double.isNaN(number)) {
				return null;
			}
			dec = new BigDecimal(number);
			return dec.toBigIntegerExact();
		} catch(ArithmeticException e) {
			return null;
		}
	}

	/*package*/ static Integer toIntExact(BigInteger i) {
		int r = i.intValue();

		if(i.equals(BigInteger.valueOf(r))) {
			return new Integer(r);
		} else {
			return null;
		}
	}

	/*package*/ static void bindLocal(
			Datum prms, Datum bcdr, Environment env, LispMessage mesg) {
		while(true) {
			if(prms == Nil.NIL) {
				if(bcdr != Nil.NIL) {
					throw mesg.getError("err.parameter.insufficient");
				}
				break;
			} else if(prms instanceof Atom) {
				if(!(prms instanceof Symbol)) {
					throw mesg.getError("err.symbol", prms);
				}
				env.bindDatum(prms, bcdr);
				break;
			} else if(!(bcdr instanceof Cons)) {
				//throw new LispException("insufficient parameter");
				throw mesg.getError("err.parameter.insufficient");
			} else if(prms instanceof Cons) {
				Datum a1 = ((Cons)prms).getCar();
				Datum a2 = ((Cons)bcdr).getCar();

				if(a1 instanceof Symbol) {
					env.bindDatum(a1, a2);
				} else if(a1 instanceof SymbolScope) {
					SymbolScope ssp = (SymbolScope)a1;

					//if(ssp.isCaptured()) {
					//	env.bindDatum(ssp.getSymbol(), a2);
					//} else {
					//	throw mesg.getError("err.symbol", a1);
					//}
					env.bindDatum(ssp.getSymbol(), a2);
				} else {
					throw mesg.getError("err.symbol", a1);
				}

				prms = ((Cons)prms).getCdr();
				bcdr = ((Cons)bcdr).getCdr();
			} else {
				//throw new LispException("Invalid type");
				throw mesg.getError("err.type.insufficient");
			}
		}
	}

	/*package*/ static Datum findListEqv(
			Datum k, Datum lst, LispMessage mesg) {
		Datum d = lst;

		while(d != Nil.NIL) {
			if(lst instanceof Cons) {
				Cons c1 = (Cons)d;

				if(c1.getCar() instanceof Symbol) {
					Symbol s = (Symbol)c1.getCar();
					Symbol r = s.getEnclosedSymbol();

					if(r == null && k == s) {
						return k;
					} else if(k == r) {
						// symbol in case
						return k;
					}
				} else if(k.isEqv(c1.getCar())) {
					return k;
				}
				d = c1.getCdr();
			} else {
				throw mesg.getError("err.list");
			}
		}
		return LispBoolean.FALSE;
	}

	private static Class<?> getCompornentType(Class<?> cl) {
		if(cl.isArray()) {
			return cl.getComponentType();
		} else if(cl.isAssignableFrom(List.class)) {
			/*TypeVariable<? extends Class<?>>[] tv;

			tv = cl.getTypeParameters();
			if(tv.length == 0) {    // raw type
				return Object.class;
			} else if(tv.length == 1) {
				return tv[0].getClass();
			} else {
				return null;
			}*/
			return null;
		} else {
			return null;
		}
	}

	/*package*/ static boolean isSameClass(Class<?> cl, Datum ld) {
		if(cl.equals(Object.class)) {
			return true;
		} else if(ld instanceof LispString) {
			return cl.isAssignableFrom(String.class);
		} else if(ld instanceof LispBoolean) {
			return (cl.isAssignableFrom(Boolean.TYPE) ||
					cl.isAssignableFrom(Boolean.class));
		} else if(ld instanceof LispCharacter) {
			return (cl.isAssignableFrom(Character.TYPE) ||
					cl.isAssignableFrom(Character.class));
		} else if(ld instanceof LispInteger) {
			return (isAssignableInt(cl)        ||
					isAssignableLong(cl)       ||
					isAssignableFloat(cl)      ||
					isAssignableDouble(cl)     ||
					isAssignableBigInteger(cl) ||
					isAssignableBigDecimal(cl));
		} else if(ld instanceof LispReal) {
			return (isAssignableInt(cl)        ||
					isAssignableLong(cl)       ||
					isAssignableFloat(cl)      ||
					isAssignableDouble(cl)     ||
					isAssignableBigInteger(cl) ||
					isAssignableBigDecimal(cl));
		} else if(ld instanceof Cons) {
			Datum p = ld;
			Class<?> cc = getCompornentType(cl);

			if(cc == null) {
				return false;
			}

			while(p != Nil.NIL) {
				if(p instanceof Cons) {
					Cons c = (Cons)p;

					if(!isSameClass(cc, c.getCar())) {
						return false;
					}
					p = c.getCdr();
				} else {
					return false;
				}
			}
			return true;
		} else if(ld instanceof LispVector) {
			Class<?> cc = getCompornentType(cl);

			if(cc == null) {
				return false;
			}

			for(Datum o : ((LispVector)ld).getList()) {
				if(!isSameClass(cc, o)) {
					return false;
				}
			}
			return true;
		} else if(ld instanceof JavaInstance) {
			return cl.isAssignableFrom(
					((JavaInstance)ld).getJavaInstance().getClass());
		} else if(ld == JavaNull.JAVA_NULL) {
			return true;
		} else if(ld == Nil.NIL) {
			return true;
		} else {
			return false;
		}
	}

	/*package*/ static boolean isSameClasses(
			Class<?>[] cl, List<Datum> ld, boolean varargs) {
		if(!varargs && cl.length != ld.size()) {
			return false;
		} else if(cl.length - 1 > ld.size()) {
			return false;
		}

		for(int i = 0; i < ld.size(); i++) {
			if(varargs && i >= cl.length - 1) {
				Class<?> lcl = cl[cl.length - 1];

				if(!lcl.isArray()) {
					return false;
				} else if(!isSameClass(
						lcl.getComponentType(), ld.get(i))) {
					return false;
				}
			} else if(!isSameClass(cl[i], ld.get(i))) {
				return false;
			}
		}
		return true;
	}

	/*package*/ static Object toJavaInstance(Class<?> cl, Datum d) {
		if(cl.equals(Object.class)) {
			return d;
		} else if(d instanceof LispString) {
			return d.getString();
		} else if(d instanceof LispBoolean) {
			return d.isTrue();
		} else if(d instanceof LispCharacter) {
			return new Character(d.getCharacter());
		} else if(d instanceof LispInteger) {
			if(isAssignableInt(cl)) {
				return new Integer(d.getInt());
			} else if(isAssignableLong(cl)) {
				return new Long(d.getLong());
			} else if(isAssignableFloat(cl)) {
				return new Float(d.getRealDouble());
			} else if(isAssignableDouble(cl)) {
				return new Double(d.getRealDouble());
			} else if(isAssignableBigInteger(cl)) {
				return d.getBigInteger();
			} else if(isAssignableBigDecimal(cl)) {
				return d.getBigDecimal();
			} else {
				throw new ClassCastException(cl.getName());
			}
		} else if(d instanceof LispReal) {
			if(isAssignableInt(cl)) {
				return new Integer(d.getInt());
			} else if(isAssignableLong(cl)) {
				return new Long(d.getLong());
			} else if(isAssignableFloat(cl)) {
				return new Float(d.getRealDouble());
			} else if(isAssignableDouble(cl)) {
				return new Double(d.getRealDouble());
			} else if(isAssignableBigInteger(cl)) {
				return d.getBigInteger();
			} else if(isAssignableBigDecimal(cl)) {
				return d.getBigDecimal();
			} else {
				throw new ClassCastException(cl.getName());
			}
		} else if(d instanceof Cons) {
			Datum         p   = d;
			Class<?>      cc  = getCompornentType(cl);
			int           len = LispUtils.consLength(d);
			ReflContainer cnt;

			if(cc == null) {
				throw new ClassCastException(cl.getName());
			} else {
				cnt = new ReflContainer(cl, len);
			}

			for(int i = 0; p != Nil.NIL; i++) {
				if(p instanceof Cons) {
					Cons c = (Cons)p;

					cnt.set(i, toJavaInstance(cc, c.getCar()));
					p = c.getCdr();
				} else {
					throw new ClassCastException(cl.getName());
				}
			}
			return cnt.toObject();
		} else if(d instanceof LispVector) {
			Class<?>      cc = getCompornentType(cl);
			int           len = LispUtils.consLength(d);
			ReflContainer cnt;

			if(cc == null) {
				throw new ClassCastException(cl.getName());
			} else {
				cnt = new ReflContainer(cl, len);
			}

			int i = 0;
			for(Datum o : ((LispVector)d).getList()) {
				cnt.set(i, toJavaInstance(cc, o));
				i++;
			}
			return cnt.toObject();
		} else if(d instanceof JavaInstance) {
			return ((JavaInstance)d).getJavaInstance();
			//return ((JavaInstance)d).getJavaInstance().getClass();
		} else if(d == JavaNull.JAVA_NULL) {
			return null;
		} else if(d == Nil.NIL) {
			return new ReflContainer(cl, 0).toObject();
			//return Collections.emptyList();
		} else {
			throw new ClassCastException(cl.getName());
		}
	}

	/*package*/ static Object[] toJavaInstances(
			Class<?>[] cl, List<Datum> ld, boolean varargs) {
		Object[] res = new Object[cl.length];
		Object   rst;

		if(!varargs && cl.length != ld.size()) {
			throw new ClassCastException();
		} else if(varargs) {
			Class<?> lcl = cl[cl.length - 1];

			if(!lcl.isArray()) {
				throw new RuntimeException();
			}
			rst = Array.newInstance(
					lcl.getComponentType(),
					ld.size() - cl.length + 1);
			res[cl.length - 1] = rst;
		} else {
			rst = null;
		}

		for(int i = 0; i < ld.size(); i++) {
			Class<?> lcl = cl[cl.length - 1];
			Class<?> lcc = lcl.getComponentType();

			if(varargs && i >= cl.length - 1) {
				Array.set(rst, i - cl.length + 1,
						toJavaInstance(lcc, ld.get(i)));
			} else if(isSameClass(cl[i], ld.get(i))) {
				res[i] = toJavaInstance(cl[i], ld.get(i));
			}
		}
		return res;
	}

	/*package*/ static Object[] toJavaInstances(
			Class<?>[] cl, boolean varargs, Datum... ld) {
		Object[] res = new Object[cl.length];
		Object   rst;

		if(!varargs && cl.length != ld.length) {
			throw new ClassCastException();
		} else if(varargs) {
			Class<?> lcl = cl[cl.length - 1];

			if(!lcl.isArray()) {
				throw new RuntimeException();
			}
			rst = Array.newInstance(
					lcl.getComponentType(),
					ld.length - cl.length + 1);
			res[cl.length - 1] = rst;
		} else {
			rst = null;
		}

		for(int i = 0; i < ld.length; i++) {
			Class<?> lcl = cl[cl.length - 1];
			Class<?> lcc = lcl.getComponentType();

			if(varargs && i >= cl.length - 1) {
				Array.set(rst, i - cl.length + 1,
						toJavaInstance(lcc, ld[i]));
			} else if(isSameClass(cl[i], ld[i])) {
				res[i] = toJavaInstance(cl[i], ld[i]);
			}
		}
		return res;
	}

	/*package*/ static Object newInstance(
			Class<?> klass, List<Datum> lst) throws ParameterNotFoundException {
		Constructor<?>[] cns = klass.getConstructors();

		for(int i = 0; i < cns.length; i++) {
			Class<?>[] cls = cns[i].getParameterTypes();
			boolean    va = cns[i].isVarArgs();

			if(IntLispUtils.isSameClasses(cls, lst, va)) {
				Object[] args =
					IntLispUtils.toJavaInstances(cls, lst, va);

				try {
					return cns[i].newInstance(args);
				} catch (IllegalArgumentException e) {
					throw new JavaException(e);
				} catch (InstantiationException e) {
					throw new JavaException(e);
				} catch (IllegalAccessException e) {
					throw new JavaException(e);
				} catch (InvocationTargetException e) {
					throw new JavaTargetException(e.getCause());
				}
			}
		}
		throw new ParameterNotFoundException();
	}

	/*package*/ static boolean isSymbolName(Datum d) {
		return (d instanceof SymbolName);
	}

	/*package*/ static void timelog(Logger _log, String str, long bef) {
		long dt = System.currentTimeMillis() - bef;
		String msec = ("000" + (dt % 1000));

		msec = msec.substring(msec.length() - 3);
		_log.fine(str + (dt / 1000) + "." + msec + "sec");
	}

	/*package*/ static void timelogFiner(
			Logger _log, String str, long bef) {
		long dt = System.currentTimeMillis() - bef;
		String msec = ("000" + (dt % 1000));

		msec = msec.substring(msec.length() - 3);
		_log.finer(str + (dt / 1000) + "." + msec + "sec");
	}

	/*package*/ static void loadJavaSubr(
			Environment env, Symbol k, String v) {
		//try {
		//	Class<?> cls = Class.forName(v);
		//	Datum ins = (Datum)cls.newInstance();
		//	
		//	if(ins instanceof Subr) {
		//		((Subr)ins).symbolName = k.getName();
		//	} else if(ins instanceof Syntax) {
		//		((Syntax)ins).symbolName = k.getName();
		//	}
		//	env.bindDatum(k, ins);
		//} catch (ClassNotFoundException e) {
		//	throw new RuntimeException(e);
		//} catch (InstantiationException e) {
		//	throw new RuntimeException(e);
		//} catch (IllegalAccessException e) {
		//	throw new RuntimeException(e);
		//}
		env.bindDatum(k, getJavaSubr(k, v));
	}

	/*package*/ static Datum getJavaSubr(Symbol k, String v) {
		try {
			Class<?> cls = Class.forName(v);
			Datum ins = (Datum)cls.newInstance();

			if(ins instanceof Subr) {
				((Subr)ins).symbolName = k.getName();
			} else if(ins instanceof Syntax) {
				((Syntax)ins).symbolName = k.getName();
			}
			return ins;
		} catch (ClassNotFoundException e) {
			throw new RuntimeException(e);
		} catch (InstantiationException e) {
			throw new RuntimeException(e);
		} catch (IllegalAccessException e) {
			throw new RuntimeException(e);
		}
	}

	//
	/*package*/ static boolean skipShebang(
			Reader pb,
			CharUnreadBuffer ub) throws IOException {
		int c, st = 0;

		while(true) {
			if((c = pb.read()) < 0) {
				return false;
			}

			switch(st) {
			case 0:  // #
				if(c != '#') {
					ub.unread((char)c);
					return true;
				}
				st = 1;
				break;
			case 1: // !
				if(c != '!') {
					ub.unread('#');
					ub.unread((char)c);
					return true;
				}
				st = 2;
				break;
			default:
				if(c == '\n') {
					return true;
				}
				break;
			}
		}
	}

	//
	/*package*/ static boolean skipShebang(
			PushbackReader pb) throws IOException {
		int c, st = 0;

		while(true) {
			if((c = pb.read()) < 0) {
				return false;
			}

			switch(st) {
			case 0:  // #
				if(c != '#') {
					pb.unread((char)c);
					return true;
				}
				st = 1;
				break;
			case 1: // !
				if(c != '!') {
					pb.unread('#');
					pb.unread((char)c);
					return true;
				}
				st = 2;
				break;
			default:
				if(c == '\n') {
					return true;
				}
				break;
			}
		}
	}

}
