/**
 * The contents of this file are subject to the Mozilla Public License Version 1.1
 * (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.mozilla.org/MPL/
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the
 * specific language governing rights and limitations under the License.
 *
 * The Original Code is "SegmentGenerator.java".  Description:
 * "This class is responsible for generating source code for HL7 segment objects"
 *
 * The Initial Developer of the Original Code is University Health Network. Copyright (C)
 * 2001.  All Rights Reserved.
 *
 * Contributor(s):  Eric Poiseau. 
 *
 * Alternatively, the contents of this file may be used under the terms of the
 * GNU General Public License (the  �GPL�), in which case the provisions of the GPL are
 * applicable instead of those above.  If you wish to allow use of your version of this
 * file only under the terms of the GPL and not to allow others to use your version
 * of this file under the MPL, indicate your decision by deleting  the provisions above
 * and replace  them with the notice and other provisions required by the GPL License.
 * If you do not delete the provisions above, a recipient may use your version of
 * this file under either the MPL or the GPL.
 *
 */
package ca.uhn.hl7v2.sourcegen;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ca.uhn.hl7v2.HL7Exception;
import ca.uhn.hl7v2.database.NormativeDatabase;
import ca.uhn.hl7v2.parser.DefaultModelClassFactory;
import ca.uhn.hl7v2.sourcegen.util.VelocityFactory;

/**
 * This class is responsible for generating source code for HL7 segment objects.
 * Each automatically generated segment inherits from AbstractSegment.
 * 
 * @author Bryan Tripp (bryan_tripp@sourceforge.net)
 * @author Eric Poiseau
 */
public class SegmentGenerator {

	private static final Logger log = LoggerFactory.getLogger(SegmentGenerator.class);

	/**
	 * <p>Creates skeletal source code (without correct data structure but no business
	 * logic) for all segments found in the normative database.  </p>
	 */
	public static void makeAll(String baseDirectory, String version, String theTemplatePackage, String theFileExt) throws IOException, SQLException, HL7Exception, MojoExecutionException {
		//make base directory
		if (!(baseDirectory.endsWith("\\") || baseDirectory.endsWith("/"))) {
			baseDirectory = baseDirectory + "/";
		}
		File targetDir = SourceGenerator.makeDirectory(baseDirectory + DefaultModelClassFactory.getVersionPackagePath(version) + "segment");

		ArrayList<String> segments = getSegmentNames(version);

		if (segments.size() == 0) {
			log.warn("No version {} segments found in database {}", 
					version, System.getProperty("ca.on.uhn.hl7.database.url"));
		}

		for (String segment : segments) {
			try {

				String hapiTestGenSegment = System.getProperty("hapi.test.gensegment");
				if (hapiTestGenSegment != null && !hapiTestGenSegment.contains(segment)) {
					continue;
				}

				makeSegment(segment, version, theTemplatePackage, targetDir, theFileExt);
			} catch (Exception e) {
//				System.err.println("Error creating source code for all segments: " + e.getMessage());
//				e.printStackTrace();
				throw new MojoExecutionException("Failure generating segments", e);
			}
		}
	}

    public static ArrayList<String> getSegmentNames(String version) throws SQLException {
        //get list of segments
		NormativeDatabase normativeDatabase = NormativeDatabase.getInstance();
		Connection conn = normativeDatabase.getConnection();
		Statement stmt = conn.createStatement();
		String sql = "SELECT seg_code, section from HL7Segments, HL7Versions where HL7Segments.version_id = HL7Versions.version_id AND hl7_version = '" + version + "'";
		//System.out.println(sql);
		ResultSet rs = stmt.executeQuery(sql);

		ArrayList<String> segments = new ArrayList<>();
		while (rs.next()) {
			String segName = rs.getString(1);

            // The DB has an invalid segment with this name
            if ("ED".equals(segName)) {
                continue;
            }

			if (Character.isLetter(segName.charAt(0))) {
				segments.add(altSegName(segName));
			}
		}
		stmt.close();
		normativeDatabase.returnConnection(conn);
        return segments;
    }

	/**
	 * <p>Returns an alternate segment name to replace the given segment name.  Substitutions
	 * made include:  </p>
	 * <ul><li>Replacing Z.. with Z</li>
	 *<li>Replacing ??? with ???</li></ul>
	 */
	public static String altSegName(String segmentName) {
		String ret = segmentName;
		if (ret.equals("Z..")) {
			ret = "Z";
		}
		if (ret.equals("CON")) {
			ret = "CON_";
		}
		return ret;
	}

	/**
	 * Returns the Java source code for a class that represents the specified segment.
	 */
	public static void makeSegment(String name, String version, String theTemplatePackage, File theTargetDir, String theFileExt) throws Exception {

		ArrayList<SegmentElement> elements = new ArrayList<>();
		String segDesc = null;
		SegmentElement se;

        NormativeDatabase normativeDatabase = NormativeDatabase.getInstance();
		try {
			Connection conn = normativeDatabase.getConnection();

//			sql.append("SELECT ");
//			sql.append("HL7SegmentDataElements.seg_code, HL7SegmentDataElements.seq_no, ");
//			sql.append("HL7SegmentDataElements.repetitional, HL7SegmentDataElements.repetitions, ");
//			sql.append("HL7DataElements.description, HL7DataElements.length, HL7DataElements.table_id, ");
//			sql.append("HL7SegmentDataElements.req_opt, HL7Segments.description, HL7DataElements.data_structure ");
//			sql.append("FROM HL7Versions RIGHT JOIN (HL7Segments INNER JOIN (HL7DataElements INNER JOIN HL7SegmentDataElements ");
//			sql.append("ON (HL7DataElements.version_id = HL7SegmentDataElements.version_id) ");
//			sql.append("AND (HL7DataElements.data_item = HL7SegmentDataElements.data_item)) ");
//			sql.append("ON (HL7Segments.version_id = HL7SegmentDataElements.version_id) ");
//			sql.append("AND (HL7Segments.seg_code = HL7SegmentDataElements.seg_code)) ");
//			sql.append("ON (HL7Versions.version_id = HL7Segments.version_id) ");
//			sql.append("WHERE HL7SegmentDataElements.seg_code = '");
//			sql.append(name);
//			sql.append("' and HL7Versions.hl7_version = '");
//			sql.append(version);
//			sql.append("' ORDER BY HL7SegmentDataElements.seg_code, HL7SegmentDataElements.seq_no;");

//			if (false) {
				listTables(conn, "HL7SegmentDataElements");
				listTables(conn, "HL7DataElements");
				listTables(conn, "HL7Segments");
//			}
			
			String lengthColName;
			if (version.startsWith("2.7") || version.startsWith("2.8")) {
				lengthColName = "HL7DataElements.max_length";
			} else {
				lengthColName = "HL7DataElements.length";
			}
			
			StringBuilder sql = new StringBuilder();
			sql.append("SELECT ");
//			sql.append("HL7SegmentDataElements.*, ");
//			sql.append("HL7DataElements.*, ");
//			sql.append("HL7Segments.* ");
			sql.append("HL7SegmentDataElements.seg_code, HL7SegmentDataElements.seq_no, ");
			sql.append("HL7SegmentDataElements.repetitional, HL7SegmentDataElements.repetitions, ");
			sql.append("HL7DataElements.description, ").append(lengthColName).append(", HL7DataElements.table_id, ");
			sql.append("HL7SegmentDataElements.req_opt, HL7Segments.description, HL7DataElements.data_structure ");
			sql.append("FROM HL7Versions RIGHT JOIN (HL7Segments INNER JOIN (HL7DataElements INNER JOIN HL7SegmentDataElements ");
			sql.append("ON (HL7DataElements.version_id = HL7SegmentDataElements.version_id) ");
			sql.append("AND (HL7DataElements.data_item = HL7SegmentDataElements.data_item)) ");
			sql.append("ON (HL7Segments.version_id = HL7SegmentDataElements.version_id) ");
			sql.append("AND (HL7Segments.seg_code = HL7SegmentDataElements.seg_code)) ");
			sql.append("ON (HL7Versions.version_id = HL7Segments.version_id) ");
			sql.append("WHERE ");
			sql.append("HL7SegmentDataElements.seg_code = '");
			sql.append(name);
			sql.append("' and HL7Versions.hl7_version = '");
			sql.append(version);
			sql.append("' ");
			sql.append("ORDER BY HL7SegmentDataElements.seg_code, HL7SegmentDataElements.seq_no;");
			//System.out.println(sql.toString());  //for debugging
			
			Statement stmt;
			ResultSet rs;
			try {
				stmt = conn.createStatement();
				rs = stmt.executeQuery(sql.toString());
			} catch (Exception e) {
				throw new MojoFailureException("Failed to execute the following SQL: " + sql.toString(), e);
			}
			List<String> usedFieldDescs = new ArrayList<>();
			int index = 0;
			while (rs.next()) {
				if (segDesc == null) {
					segDesc = rs.getString(9);
				}
				se = new SegmentElement(name, version, index++);
				se.field = rs.getInt(2);
				se.rep = rs.getString(3);
				se.repetitions = rs.getInt(4);
				if (se.repetitions == 0) {
					if (se.rep == null || !se.rep.equalsIgnoreCase("Y")) {
						se.repetitions = 1;
					}
				}
				se.desc = rs.getString(5);

				// If two fields have the same name, add "Rep 1" or "Rep 2" etc to the name
				String originalSeDesc = se.desc;
				if (usedFieldDescs.contains(se.desc)) {
					se.desc = se.desc + " Number " + (Collections.frequency(usedFieldDescs, originalSeDesc) + 1);
				}
				usedFieldDescs.add(originalSeDesc);

				se.length = rs.getInt(6);
				se.table = rs.getInt(7);
				se.opt = rs.getString(8);
				se.type = rs.getString(10);
				//shorten CE_x to CE
				if (se.type.startsWith("CE")) {
					se.type = "CE";
				}

				// Fix problems
				if (se.type.equals("-") || se.type.equals("NUL")) {
					se.type = "NULLDT";
				}

				/*
				 * ***
				 * index is 1-indexed here!!
				 * ***
				 */
				
				// 3454369
				if (version.equals("2.3") && name.equals("MRG") && index == 7) {
					se.type = "XPN";
				}

				// https://sourceforge.net/p/hl7api/bugs/95/
				if (version.equals("2.3") && name.equals("ORC") && index == 14) {
					se.type = "XTN";
				}

				// 2864817
				if (version.equals("2.3") && name.equals("PID") && index == 5) {
					se.rep = "Y";
					se.repetitions = -1;
				}

				elements.add(se);
				/*System.out.println("Segment: " + name + " Field: " + se.field + " Rep: " + se.rep +
				" Repetitions: " + se.repetitions + " Desc: " + se.desc + " Length: " + se.length +
				" Table: " + se.table + " Segment Desc: " + segDesc);*/
			}
			stmt.close();
			normativeDatabase.returnConnection(conn);
		} catch (SQLException sqle) {
//			sqle.printStackTrace();
//			return;
			throw new MojoFailureException("Failed to generate segment", sqle);
		}
		
		String fileName = theTargetDir.toString() + "/" + name + "." + theFileExt;
		
		String basePackageName = DefaultModelClassFactory.getVersionPackageName(version);
		String[] datatypePackages = { basePackageName + "datatype" };
        writeSegment(fileName, version, name, elements, segDesc, basePackageName, datatypePackages, theTemplatePackage);

	}

	private static void listTables(Connection conn, String tableName) throws MojoFailureException, SQLException {
		{
			StringBuilder sql = new StringBuilder();
			sql.append("SELECT ");
			sql.append(tableName).append(".* ");
			sql.append("FROM ").append(tableName).append(" ");
			//System.out.println(sql.toString());  //for debugging
			
			Statement stmt;
			ResultSet rs;
			try {
				stmt = conn.createStatement();
				rs = stmt.executeQuery(sql.toString());
			} catch (Exception e) {
				throw new MojoFailureException("Failed to execute the following SQL: " + sql.toString(), e);
			}

			rs.next();
			Set<String> cols = new HashSet<>();
			for (int i = 0; i < rs.getMetaData().getColumnCount(); i++) {
				cols.add(rs.getMetaData().getColumnName(i+1));
			}
//			System.out.println();
//			System.out.println(tableName + "Cols: " + cols);
//			System.out.println();
			
			rs.close();
			stmt.close();
		}
		
	
	}



	public static void writeSegment(String fileName, String version, String segmentName, ArrayList<SegmentElement> elements, String description, String basePackage, String[] datatypePackages, String theTemplatePackage) throws Exception {
		log.debug("Writing segment: {}", fileName);
		
		BufferedWriter out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(fileName, false), SourceGenerator.ENCODING));
		
        theTemplatePackage = theTemplatePackage.replace(".", "/");
        Template template = VelocityFactory.getClasspathTemplateInstance(theTemplatePackage + "/segment.vsm");
        VelocityContext ctx = new VelocityContext();
        ctx.put("segmentName", segmentName);
        ctx.put("typeDescription", description);
        ctx.put("basePackageName", basePackage);
        ctx.put("elements", elements);
        ctx.put("datatypePackages", datatypePackages);
        ctx.put("hl7VersionInQuotes", '"' + version + '"');
        
        template.merge(ctx, out);
		
//      String string = createSegmentString(version, segmentName, elements, description, basePackage, datatypePackageString);
//      out.write(string);
		
		out.flush();
		out.close();
	}
	
	
}
