/*******************************************************************************
 * Copyright (c) 2015 Jeff Martin.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Lesser General Public
 * License v3.0 which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/lgpl.html
 * 
 * Contributors:
 *     Jeff Martin - initial API and implementation
 ******************************************************************************/
package cuchaz.enigma.bytecode.accessors;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

import cuchaz.enigma.bytecode.InfoType;

public class ConstInfoAccessor {
	
	private static Class<?> m_class;
	private static Field m_index;
	private static Method m_getTag;
	
	static {
		try {
			m_class = Class.forName("javassist.bytecode.ConstInfo");
			m_index = m_class.getDeclaredField("index");
			m_index.setAccessible(true);
			m_getTag = m_class.getMethod("getTag");
			m_getTag.setAccessible(true);
		} catch (Exception ex) {
			throw new Error(ex);
		}
	}
	
	private Object m_item;
	
	public ConstInfoAccessor(Object item) {
		if (item == null) {
			throw new IllegalArgumentException("item cannot be null!");
		}
		m_item = item;
	}
	
	public ConstInfoAccessor(DataInputStream in) throws IOException {
		try {
			// read the entry
			String className = in.readUTF();
			int oldIndex = in.readInt();
			
			// NOTE: ConstInfo instances write a type id (a "tag"), but they don't read it back
			// so we have to read it here
			in.readByte();
			
			Constructor<?> constructor = Class.forName(className).getConstructor(DataInputStream.class, int.class);
			constructor.setAccessible(true);
			m_item = constructor.newInstance(in, oldIndex);
		} catch (IOException ex) {
			throw ex;
		} catch (Exception ex) {
			throw new Error(ex);
		}
	}
	
	public Object getItem() {
		return m_item;
	}
	
	public int getIndex() {
		try {
			return (Integer)m_index.get(m_item);
		} catch (Exception ex) {
			throw new Error(ex);
		}
	}
	
	public void setIndex(int val) {
		try {
			m_index.set(m_item, val);
		} catch (Exception ex) {
			throw new Error(ex);
		}
	}
	
	public int getTag() {
		try {
			return (Integer)m_getTag.invoke(m_item);
		} catch (Exception ex) {
			throw new Error(ex);
		}
	}
	
	public ConstInfoAccessor copy() {
		return new ConstInfoAccessor(copyItem());
	}
	
	public Object copyItem() {
		// I don't know of a simpler way to copy one of these silly things...
		try {
			// serialize the item
			ByteArrayOutputStream buf = new ByteArrayOutputStream();
			DataOutputStream out = new DataOutputStream(buf);
			write(out);
			
			// deserialize the item
			DataInputStream in = new DataInputStream(new ByteArrayInputStream(buf.toByteArray()));
			Object item = new ConstInfoAccessor(in).getItem();
			in.close();
			
			return item;
		} catch (Exception ex) {
			throw new Error(ex);
		}
	}
	
	public void write(DataOutputStream out) throws IOException {
		try {
			out.writeUTF(m_item.getClass().getName());
			out.writeInt(getIndex());
			
			Method method = m_item.getClass().getMethod("write", DataOutputStream.class);
			method.setAccessible(true);
			method.invoke(m_item, out);
		} catch (IOException ex) {
			throw ex;
		} catch (Exception ex) {
			throw new Error(ex);
		}
	}
	
	@Override
	public String toString() {
		try {
			ByteArrayOutputStream buf = new ByteArrayOutputStream();
			PrintWriter out = new PrintWriter(buf);
			Method print = m_item.getClass().getMethod("print", PrintWriter.class);
			print.setAccessible(true);
			print.invoke(m_item, out);
			out.close();
			return buf.toString().replace("\n", "");
		} catch (Exception ex) {
			throw new Error(ex);
		}
	}
	
	public InfoType getType() {
		return InfoType.getByTag(getTag());
	}
}