package org.arefgard.container;

import java.io.IOException;
import java.io.InputStream;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
 * DIコンテナを生成するファクトリ。
 * 
 * @author T.A.K.A.Dark
 *
 */
public class ContainerFactory {
	
	private static Log log = LogFactory.getLog(ContainerFactory.class);
	
	private static String DEFAULT_CONFIG = "components.xml";
	
	private static String XPATH_NODE_IMPORT = "/container/import";
	private static String XPATH_NODE_JDBC_DRIVER = "/container/connection/property[@name='jdbc.driver']/@value";
	private static String XPATH_NODE_JDBC_URL = "/container/connection/property[@name='jdbc.url']/@value";
	private static String XPATH_NODE_JDBC_USER = "/container/connection/property[@name='jdbc.user']/@value";
	private static String XPATH_NODE_JDBC_PASSWORD = "/container/connection/property[@name='jdbc.password']/@value";
	private static String XPATH_NODE_JDBC_DIALECT = "/container/connection/property[@name='jdbc.dialect']/@value";
	private static String XPATH_NODE_BUSINESS = "/container/business";
	private static String XPATH_NODE_SERVICE = "service";
	private static String XPATH_NODE_DOMAIN = "/container/model";
	private static String XPATH_NODE_INPUT = "input";
	private static String XPATH_NODE_COMPONENT = "component";
	private static String XPATH_NODE_COMPROPERTY = "property";
	private static String XPATH_NODE_QUERY = "query";
	private static String XPATH_NODE_FLOW = "flow";
	private static String XPATH_NODE_PARAMETER = "parameter";
	private static String XPATH_NODE_OUTPUT = "output";
	private static String XPATH_NODE_BEAN = "bean";
	private static String XPATH_NODE_PROPERTY = "property";
	
	private static String XPATH_ATTR_NAME = "string(@name)";
	private static String XPATH_ATTR_CLASS = "string(@class)";
	private static String XPATH_ATTR_REF = "string(@ref)";
	private static String XPATH_ATTR_TYPE = "string(@type)";
	private static String XPATH_ATTR_TABLE = "string(@table)";
	private static String XPATH_ATTR_COLUMN = "string(@column)";
	private static String XPATH_ATTR_TEXT = "text()";
	
	private static XPathExpression XPATH_IMPORT;
	private static XPathExpression XPATH_JDBC_DRIVER;
	private static XPathExpression XPATH_JDBC_URL;
	private static XPathExpression XPATH_JDBC_USER;
	private static XPathExpression XPATH_JDBC_PASSWORD;
	private static XPathExpression XPATH_JDBC_DIALECT;
	private static XPathExpression XPATH_BUSINESS;
	private static XPathExpression XPATH_SERVICE;
	private static XPathExpression XPATH_DOMAIN;
	private static XPathExpression XPATH_INPUT;
	private static XPathExpression XPATH_COMPONENT;
	private static XPathExpression XPATH_COMPROPERTY;
	private static XPathExpression XPATH_QUERY;
	private static XPathExpression XPATH_FLOW;
	private static XPathExpression XPATH_PARAMETER;
	private static XPathExpression XPATH_OUTPUT;
	private static XPathExpression XPATH_BEAN;
	private static XPathExpression XPATH_PROPERTY;
	
	private static XPathExpression XPATH_NAME;
	private static XPathExpression XPATH_CLASS;
	private static XPathExpression XPATH_REF;
	private static XPathExpression XPATH_TYPE;
	private static XPathExpression XPATH_TABLE;
	private static XPathExpression XPATH_COLUMN;
	private static XPathExpression XPATH_TEXT;
	
	private static boolean isInitialized = false;

	static {
		XPathFactory factory = XPathFactory.newInstance();
		XPath xpath = factory.newXPath();
		try {
			XPATH_IMPORT = xpath.compile(XPATH_NODE_IMPORT);
			XPATH_JDBC_DRIVER = xpath.compile(XPATH_NODE_JDBC_DRIVER);
			XPATH_JDBC_URL = xpath.compile(XPATH_NODE_JDBC_URL);
			XPATH_JDBC_USER = xpath.compile(XPATH_NODE_JDBC_USER);
			XPATH_JDBC_PASSWORD = xpath.compile(XPATH_NODE_JDBC_PASSWORD);
			XPATH_JDBC_DIALECT = xpath.compile(XPATH_NODE_JDBC_DIALECT);
			XPATH_BUSINESS = xpath.compile(XPATH_NODE_BUSINESS);
			XPATH_SERVICE = xpath.compile(XPATH_NODE_SERVICE);
			XPATH_DOMAIN = xpath.compile(XPATH_NODE_DOMAIN);
			XPATH_INPUT = xpath.compile(XPATH_NODE_INPUT);
			XPATH_COMPONENT = xpath.compile(XPATH_NODE_COMPONENT);
			XPATH_COMPROPERTY = xpath.compile(XPATH_NODE_COMPROPERTY);
			XPATH_QUERY = xpath.compile(XPATH_NODE_QUERY);
			XPATH_FLOW = xpath.compile(XPATH_NODE_FLOW);
			XPATH_PARAMETER = xpath.compile(XPATH_NODE_PARAMETER);
			XPATH_OUTPUT = xpath.compile(XPATH_NODE_OUTPUT);
			XPATH_BEAN = xpath.compile(XPATH_NODE_BEAN);
			XPATH_PROPERTY = xpath.compile(XPATH_NODE_PROPERTY);
			
			XPATH_NAME = xpath.compile(XPATH_ATTR_NAME);
			XPATH_CLASS = xpath.compile(XPATH_ATTR_CLASS);
			XPATH_REF = xpath.compile(XPATH_ATTR_REF);
			XPATH_TYPE = xpath.compile(XPATH_ATTR_TYPE);
			XPATH_TABLE = xpath.compile(XPATH_ATTR_TABLE);
			XPATH_COLUMN = xpath.compile(XPATH_ATTR_COLUMN);
			XPATH_TEXT = xpath.compile(XPATH_ATTR_TEXT);
		}catch(Exception e) {
			log.error("XPath式のコンパイルに失敗しました", e);
		}
	}
	
	public static void initialize() throws ContainerException {
		initialize(DEFAULT_CONFIG);
	}
	
	public static void initialize(String name) 
		throws ContainerException {
		if(isInitialized() == true) {
			return;
		}
		init(name);
	}
	
	private static void init(String name) 
		throws ContainerException {
		IceryaContainer socContainer = newContainer();
		
		log.debug("読み込みコンポーネントファイル:" + name);
		InputStream is = ClassLoader.getSystemResourceAsStream(name);
		if(is == null) {
			is = ClassLoader.getSystemResourceAsStream(DEFAULT_CONFIG);
			if(is == null) {
				throw new ContainerException("コンポーネントファイル:" + name + "が見つかりません");
			}
		}
		
		Document doc = parseDocument(is);
		
		try {
			
			parseImport(doc);
			
			// コネクション
			ConnectionDef connectionDef = new ConnectionDef();
			connectionDef.setDriver(((Node)XPATH_JDBC_DRIVER.evaluate(doc, XPathConstants.NODE)).getNodeValue());
			connectionDef.setUrl(((Node)XPATH_JDBC_URL.evaluate(doc, XPathConstants.NODE)).getNodeValue());
			connectionDef.setUser(((Node)XPATH_JDBC_USER.evaluate(doc, XPathConstants.NODE)).getNodeValue());
			connectionDef.setPassword(((Node)XPATH_JDBC_PASSWORD.evaluate(doc, XPathConstants.NODE)).getNodeValue());
			connectionDef.setDialect(((Node)XPATH_JDBC_DIALECT.evaluate(doc, XPathConstants.NODE)).getNodeValue());

			socContainer.setConnectionDef(connectionDef);
			
			// BusinessDomain部をパース
			parseBusinessDomain(socContainer, doc);
			
			// DomainModel部をパース
			parseDomainModel(socContainer, doc);
		}catch(ContainerException e) {
			log.error("Beanが存在しません", e);
			throw e;
		}catch(XPathExpressionException e) {
			log.error("XPath式のコンパイルに失敗しました", e);
			throw new ContainerException("XPath式のコンパイルに失敗しました", e);
		}catch(Exception e) {
			log.error("想定外の例外が発生しました", e);
			throw new ContainerException("想定外の例外が発生しました", e);
		}
		isInitialized = true;
	}

	/**
	 * @param doc
	 * @throws XPathExpressionException
	 * @throws ContainerException
	 */
	private static void parseImport(Document doc) throws XPathExpressionException, ContainerException {
		NodeList importNodes = (NodeList)XPATH_IMPORT.evaluate(doc, 
																	   XPathConstants.NODESET);
		int importLen = importNodes.getLength();
		for(int i = 0; i < importLen; i++) {
			Node importNode = importNodes.item(i);
			log.debug("インポート発見：" + getRef(importNode));
			init(getRef(importNode));
		}
	}

	/**
	 * @param socContainer
	 * @param doc
	 * @throws XPathExpressionException
	 * @throws ContainerException
	 */
	private static void parseBusinessDomain(IceryaContainer socContainer, Document doc) throws XPathExpressionException, ContainerException {
		NodeList businessNodes = (NodeList)XPATH_BUSINESS.evaluate(doc,
																		XPathConstants.NODESET);
		int businessLen = businessNodes.getLength();
		for(int i = 0; i < businessLen; i++) {
			Node businessNode = businessNodes.item(i);
			String businessName = getName(businessNode);
			log.debug("ビジネスドメイン発見:" + businessName);
			BusinessDef business = new BusinessDef(businessName);
			
			// Serviceの取得
			NodeList serviceNodes = (NodeList)XPATH_SERVICE.evaluate(businessNode,
																			 XPathConstants.NODESET);
			int serviceLen = serviceNodes.getLength();
			for (int j = 0; j < serviceLen; j++) {
				Node serviceNode = serviceNodes.item(j);
				String serviceName = getName(serviceNode);
				log.debug("サービス発見:" + serviceName);
				ServiceDef service = new ServiceDef(serviceName);
				
				// inputの取得
				NodeList inputNodes = (NodeList)XPATH_INPUT.evaluate(serviceNode,
																			 XPathConstants.NODESET);
				int inputLen = inputNodes.getLength();
				for(int k = 0; k < inputLen; k++) {
					Node inputNode = inputNodes.item(k);
					String inputName = getName(inputNode);
					String inputClass = getClass(inputNode);
					log.debug("input発見:" + inputName + "," + inputClass);
					service.addInputDef(new InputDef(inputName, inputClass));
				}
				
				// サービスに対するDI構成の取得
				NodeList componentNodes = (NodeList)XPATH_COMPONENT.evaluate(serviceNode, XPathConstants.NODESET);
				int componentLen = inputNodes.getLength();
				for(int k = 0; k < componentLen; k++) {
					Node componentNode = componentNodes.item(k);
					String componentName = getName(componentNode);
					String componentRef = getRef(componentNode);
					log.debug("component発見:" + componentName + "," + componentRef);
					ComponentDef componentDef = new ComponentDef(componentName, componentRef);
					
					NodeList propertyNodes = (NodeList)XPATH_COMPROPERTY.evaluate(componentNode, XPathConstants.NODESET);
					int propertyLen = propertyNodes.getLength();
					for(int l = 0; l < propertyLen; l++) {
						Node propertyNode = propertyNodes.item(l);
						String propertyName = getName(propertyNode);
						String propertyValue = getText(propertyNode);
						log.debug("property発見:" + propertyName + "," + propertyValue);
						componentDef.addPropertyDef(new ComponentPropertyDef(propertyName, propertyValue));
					}
					Node queryNode = (Node)XPATH_QUERY.evaluate(componentNode, XPathConstants.NODE);
					String query = getText(queryNode);
					if(query != null && !query.equals("")) {
						log.debug("query発見:" + query);
						componentDef.setQuery(query);
					}
					service.addComponentDef(componentDef);
				}
				
				
				// ワークフロー実行構成の取得
				Node flowNode = (Node)XPATH_FLOW.evaluate(serviceNode, XPathConstants.NODE);
				String flowName = getName(flowNode);
				
				FlowDef flowDef = new FlowDef(flowName);
				log.debug("flow発見:" + flowName);
				NodeList parameterNodes = (NodeList)XPATH_PARAMETER.evaluate(flowNode, XPathConstants.NODESET);
				int parameterLen = parameterNodes.getLength();
				for(int k = 0; k < parameterLen; k++) {
					Node parameterNode = parameterNodes.item(k);
					String parameterName = getName(parameterNode);
					String parameterRef = getRef(parameterNode);
					log.debug("parameter発見:" + parameterName + "," + parameterRef);
					flowDef.addParameterDef(new ParameterDef(parameterName, parameterRef));
				}
				service.setFlowDef(flowDef);
				
				// outputの取得
				NodeList outputNodes = (NodeList)XPATH_OUTPUT.evaluate(serviceNode,
						 													   XPathConstants.NODESET);
				int outputLen = outputNodes.getLength();
				for(int k = 0; k < outputLen; k++) {
					Node outputNode = outputNodes.item(k);
					String outputName = getName(outputNode);
					String outputRef = getRef(outputNode);
					log.debug("output発見:" + outputName + "," + outputRef);
					service.addOutputDef(new OutputDef(outputName, outputRef));
				}
				
				business.addServiceDef(service);
			}
			socContainer.addBusinessDef(business);
		}
	}
	
	/**
	 * @param socContainer
	 * @param doc
	 * @throws XPathExpressionException
	 * @throws ContainerException
	 */
	private static void parseDomainModel(IceryaContainer socContainer, Document doc) throws XPathExpressionException, ContainerException {
		NodeList domainNodes = (NodeList)XPATH_DOMAIN.evaluate(doc,
				 													   XPathConstants.NODESET);
		int domainLen = domainNodes.getLength();
		for(int i = 0;i < domainLen;i++) {
			Node domainNode = domainNodes.item(i);
			DomainDef domain = new DomainDef(getName(domainNode));
			// Beanの取得
			NodeList beanNodes = getBean(domainNode);
			int beanLen = beanNodes.getLength();
			for(int j = 0; j < beanLen; j++) {
				Node beanNode = beanNodes.item(j);
				String beanName = getName(beanNode);
				String beanClass = getClass(beanNode);
				String beanTable = getTable(beanNode);
				log.debug("Bean発見：" + beanName + "," + beanClass + "," + beanTable);
				BeanDef bean = new BeanDef(beanName, beanClass);
				bean.setTable(beanTable);
				
				NodeList propNodes = getProperty(beanNode);
				
				int propLen = propNodes.getLength();
				for(int k = 0; k < propLen; k++) {
					Node propNode = propNodes.item(k);
					String propName = getName(propNode);
					String propType = getType(propNode);
					String propColumn = getColumn(propNode);
					log.debug("Property発見:" + propName + "," + propType + "," + propColumn);
					bean.addProperty(new PropertyDef(propName, propType, propColumn));
				}
				domain.addBeanDef(bean);
			}
			socContainer.addDomainDef(domain);
		}
	}

	private static Document parseDocument(InputStream is) throws ContainerException {
		DocumentBuilderFactory factory;
		Document doc = null;
		try {
			factory = DocumentBuilderFactory.newInstance();
			DocumentBuilder builder = factory.newDocumentBuilder();
			doc = builder.parse(is);
		}catch(SAXException e) {
			log.error("構成ファイルのパースに失敗しました", e);
			throw new ContainerException("構成ファイルのパースに失敗しました", e);
		}catch(ParserConfigurationException e) {
			log.error("XMLパーサの構成に失敗しました", e);
			throw new ContainerException("XMLパーサの構成に失敗しました", e);
		}catch(IOException e) {
			log.error("XMLファイルの読み込みに失敗しました", e);
			throw new ContainerException("XMLファイルの読み込みに失敗しました", e);
		}
		return doc;
	}
	
	private static synchronized boolean isInitialized() {
		return isInitialized;
	}
	
	private static NodeList getBean(Object obj) throws XPathExpressionException {
		return (NodeList)XPATH_BEAN.evaluate(obj, XPathConstants.NODESET);
	}
	
	private static NodeList getProperty(Object obj) throws XPathExpressionException {
		return (NodeList)XPATH_PROPERTY.evaluate(obj, XPathConstants.NODESET);
	}
	
	private static String getName(Object obj) throws XPathExpressionException {
		return (String)XPATH_NAME.evaluate(obj, XPathConstants.STRING);
	}
	
	private static String getClass(Object obj) throws XPathExpressionException {
		return (String)XPATH_CLASS.evaluate(obj, XPathConstants.STRING);
	}
	
	private static String getRef(Object obj) throws XPathExpressionException {
		return (String)XPATH_REF.evaluate(obj, XPathConstants.STRING);
	}
	
	private static String getType(Object obj) throws XPathExpressionException {
		return (String)XPATH_TYPE.evaluate(obj, XPathConstants.STRING);
	}
	
	private static String getTable(Object obj) throws XPathExpressionException {
		return (String)XPATH_TABLE.evaluate(obj, XPathConstants.STRING);
	}
	
	private static String getColumn(Object obj) throws XPathExpressionException {
		return (String)XPATH_COLUMN.evaluate(obj, XPathConstants.STRING);
	}
	
	private static String getText(Object obj) throws XPathExpressionException {
		return (String)XPATH_TEXT.evaluate(obj, XPathConstants.STRING);
	}
	
	public static IceryaContainer newContainer()
		throws ContainerException {		
		IceryaContainer container = SingletonIceryaContainer.newInstance();
		return container;
	}
}
