/*
 * 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.nano;

import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * 
 *
 *
 * @author MORIGUCHI, Yuichiro 2009
 */
public final class Environment {

	//
	private Environment rootenv;
	private Map<SymbolName, Datum> binds = Collections.synchronizedMap(
			new HashMap<SymbolName, Datum>());
	private Set<Datum> rdonly = Collections.synchronizedSet(
			new HashSet<Datum>());

	/**
	 * 
	 */
	public Environment() {
		rootenv = null;
	}

	/**
	 * 
	 * @param rootenv
	 */
	public Environment(Environment rootenv) {
		this.rootenv = rootenv;
	}

	/**
	 * 
	 * @param sym
	 * @param d
	 */
	public void bindDatum(Datum sym, Datum d) {
		if(sym instanceof SymbolName) {
			binds.put((SymbolName)sym, d);
			if(d instanceof Subr) {
				((Subr)d).symbolName = ((SymbolName)sym).getName();
			}
		} else {
			throw new LispException("Parameter is not a symbol");
		}
	}

	/**
	 * 
	 * @param sym
	 * @param d
	 */
	public void bindDatum(String sym, Datum d) {
		bindDatum(Symbol.getSymbol(sym), d);
	}

	//
	/*package*/ void bindDatumReadOnly(Datum sym, Datum d) {
		bindDatum(sym, d);
		rdonly.add(sym);
	}

	//
	/*package*/ void bindDatumWithoutScope(Datum sym, Datum d) {
		if(sym instanceof SymbolName) {
			binds.put(((SymbolName)sym).getSymbol(), d);
		} else {
			throw new LispException("Parameter is not a symbol");
		}
	}

	/**
	 * 
	 * @param sym
	 * @return
	 */
	public Datum getDatum(Datum sym) {
		if(sym instanceof Symbol) {
			return binds.get(sym);
		} else {
			throw new LispException("Parameter is not a symbol");
		}
	}

	/**
	 * 
	 * @param sym
	 * @return
	 */
	public Datum getDatumTop(Datum sym) {
		if(sym instanceof Symbol) {
			return getGlobal().binds.get(sym);
		} else {
			throw new LispException("Parameter is not a symbol");
		}
	}

	/**
	 * 
	 * @param sym
	 * @return
	 */
	public Datum removeDatum(Datum sym) {
		if(sym instanceof Symbol) {
			return binds.remove(sym);
		} else {
			throw new LispException("Parameter is not a symbol");
		}
	}

	/**
	 * 
	 * @param sym
	 * @return
	 */
	public Datum findDatum(Datum sym) {
		if(sym instanceof SymbolName) {
			Environment env = this;
			SymbolName s1 = (SymbolName)sym;

			while(env != null) {
				Datum f = env.binds.get(sym);

				if(f != null) {
					return f;
				} else if((f = env.binds.get(
						Symbol.DEFAULT_NAMESPACE.getSymbol(
								s1.getName()))) != null) {
					return f;
				}
				env = env.rootenv;
			}
			return null;
		} else {
			throw new LispException("Parameter is not a symbol");
		}
	}

	/**
	 * 
	 * @param sym
	 * @param d
	 * @return
	 * @throws ReadOnlyException
	 */
	public boolean setDatum(
			Datum sym, Datum d) throws ReadOnlyException {
		if(sym instanceof Symbol) {
			Environment env = this;
			Symbol s1 = (Symbol)sym;

			while(env != null) {
				Datum f = env.binds.get(sym);

				if(f != null) {
					if(rdonly.contains(sym)) {
						throw new ReadOnlyException();
					}
					env.binds.put((Symbol)sym, d);
					return true;
				} else {
					Symbol s0 = Symbol.DEFAULT_NAMESPACE.getSymbol(
							s1.getName());

					if((f = env.binds.get(s0)) != null) {
						if(rdonly.contains(sym)) {
							throw new ReadOnlyException();
						}
						env.binds.put(s0, d);
						return true;
					}
				}
				env = env.rootenv;
			}
			//throw new NotBoundException();
			return false;
		} else {
			throw new LispException("Parameter is not a symbol");
		}
	}

	/**
	 * 
	 * @return
	 */
	public Map<SymbolName, Datum> getBoundData() {
		return Collections.unmodifiableMap(binds);
	}

	/**
	 * 
	 * @return
	 */
	public Environment getGlobal() {
		Environment env = this;

		while(env.rootenv != null) {
			env = env.rootenv;
		}
		return env;
	}

	/**
	 * 
	 * @return
	 */
	public Environment getRootenv() {
		return rootenv;
	}

	//
	/*package*/ Environment copy() {
		Environment res = new Environment();

		res.rootenv = rootenv;
		res.binds   = new HashMap<SymbolName, Datum>(binds);
		return res;
	}

	//
	/*package*/ Environment copyNotRoot() {
		return (rootenv != null) ? copy() : this;
	}

	//
	private static Environment _copyExceptRoot(Environment e) {
		Environment er;

		if(e.rootenv != null) {
			er = new Environment(_copyExceptRoot(e.rootenv));
			er.binds = new HashMap<SymbolName, Datum>(e.binds);
			return er;
		} else {
			return e;
		}
	}

	//
	Environment copyExceptRoot() {
		return _copyExceptRoot(this);
	}

	/**
	 * 
	 */
	public boolean isFreeVariable(Datum x) {
		return (x instanceof Symbol &&
				!((Symbol)x).isMacroBound() && findDatum(x) == null);
	}

	/**
	 * 
	 */
	public boolean isBoundVariable(Datum x) {
		return (x instanceof Symbol &&
				(((Symbol)x).isMacroBound() || findDatum(x) != null));
	}

	/* (non-Javadoc)
	 * @see java.lang.Object#toString()
	 */
	public String toString() {
		Environment env = this;
		StringBuilder buf = new StringBuilder();

		while(env.rootenv != null) {
			buf.append(env.binds).append("->");
			env = env.rootenv;
		}
		buf.append("{Global}");
		return buf.toString();
	}

}
