package ciss.phase_viewer.atomcoord.pmodel;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Cif {
    private static final String DELIMITER = " \t\n\r#";
    private static final boolean DEBUG = false;

    enum CrystalSystem {
        TRICLINIC, MONOCLINIC, ORTHORHOMBIC, TETRAGONAL, TRIGONAL, RHOMBOHEDRAL, HEXAGONAL, CUBIC, UNDEFINED
    };

    private enum LoopType {
        ATOM, OPERATOR, UNDEFINED
    };

    private static final String[] tag_cell_length = new String[] { "length_a",
            "length_b", "length_c" };
    private static final String[] tag_cell_angle = new String[] {
            "angle_alpha", "angle_beta", "angle_gamma" };
    private static final String[] tag_atom_site = new String[] { "fract_x",
            "fract_y", "fract_z", "label", "type_symbol" };

    private static final String[] tag_ignore_loop = new String[] {
            "_publ_author_name", "_atom_site_aniso_label" };

    private int sgn; // Space-Group Number
    private CrystalSystem crystalSystem;
    private String HM;

    private double[] cellLength;
    private double[] cellAngle;

    ArrayList<Atom> atomList;
    ArrayList<String> symmetryEquivPos;

    Cif() {
        sgn = -1; // undefined (0)
        crystalSystem = CrystalSystem.UNDEFINED;
        HM = "UNDEFINED";
        cellLength = new double[3];
        cellAngle = new double[3];
        atomList = new ArrayList<Atom>();
        symmetryEquivPos = new ArrayList<String>();
    }

    int getIntTablesNum() {
        return this.sgn;
    }

    String getHermannMauguin() {
        return HM;
    }

    double[] getCellLength() {
        double[] length = new double[3];

        for (int i = 0; i < 3; ++i)
            length[i] = cellLength[i];

        return length;
    }

    double[] getCellAngle() {
        double[] angle = new double[3];

        for (int i = 0; i < 3; ++i)
            angle[i] = cellAngle[i];

        return angle;
    }

    int getNumberOfAtom() {
        return this.atomList.size();
    }

    Atom getAtom(int n) {
        return this.atomList.get(n);
    }

    public static void main(String args[]) {
        if (args.length != 1) {
            System.out.println("Usage: java CIF CIF_file_name");
            System.exit(1);
        }

        Cif pCIF = new Cif();
        int ndb = -999;

        String fileName = args[0];
        System.out.println("CIF file name: " + fileName);
        try {
            ndb = getMultiplicity(fileName);
            // <SELECT_DATA_BLOCK>
            if (ndb == 0) {
                System.out.println("Warning : No 'data_' tag found.");
                System.out.println("Read the CIF file forcibly.");
            } else if (ndb == 1) {
                ; // nothing to do
            } else {
                // SELECT
                System.out.println("Number of data block in the CIF: " + ndb);
                BufferedReader br = new BufferedReader(new InputStreamReader(
                        System.in));
                String str = br.readLine();
                ndb = Integer.parseInt(str);
                System.out.println("Selected: " + ndb);
            }
            // </SELECT_DATA_BLOCK>

            pCIF.read(fileName, ndb);
            pCIF.check();
        } catch (PmodelException ex) {
            System.out.println("ERROR!!!");
            ex.printStackTrace();
            System.exit(1);
        } catch (IOException ex) {
            System.out.println("ERROR!!!");
            ex.printStackTrace();
            System.exit(1);
        }

        System.out.println("Summary:");
        System.out.println("Data Block: " + ndb);
        System.out.println("Cell System: " + pCIF.crystalSystem);
        System.out.println("Symmetry info.");
        System.out.println(pCIF.HM + "\t" + pCIF.sgn);
        System.out.println("Length:");
        for (int i = 0; i < 3; ++i)
            System.out.println(pCIF.cellLength[i]);
        System.out.println("Angle:");
        for (int i = 0; i < 3; ++i)
            System.out.println(pCIF.cellAngle[i]);
        System.out.println("Atom(s): " + pCIF.atomList.size());
        for (int i = 0; i < pCIF.atomList.size(); ++i)
            System.out.println(pCIF.atomList.get(i));
        System.out.println("Symmetry operation(s): "
                + pCIF.symmetryEquivPos.size());
        for (int i = 0; i < pCIF.symmetryEquivPos.size(); ++i)
            System.out.println(pCIF.symmetryEquivPos.get(i));
    }

    static int getMultiplicity(String fileName) throws PmodelException {
        return getDataBlockTitle(fileName).length;
    }

    public static String[] getDataBlockTitle(String fileName)
            throws PmodelException {
        ArrayList<String> titles = new ArrayList<String>(1);
        try {
            BufferedReader br = new BufferedReader(new FileReader(fileName));

            String line;
            while ((line = br.readLine()) != null) { // read all lines
                line = line.trim();
                if (line.length() < 5)
                    continue;
                if (line.substring(0, 5).equalsIgnoreCase("data_"))
                    titles.add(line);
            }
            br.close();
        } catch (IOException e) {
            PmodelException ex = new PmodelException();
            ex.message = "Cannot read the CIF file.";
            throw ex;
        }
        return (String[]) titles.toArray(new String[0]);
    }

    void read(String fileName) throws PmodelException {
        this.read(fileName, 1);
    }

    void read(String fileName, int ndb) throws PmodelException {
        if (getMultiplicity(fileName) < ndb) {
            PmodelException ex = new PmodelException();
            throw ex;
        }

        ArrayList<String> al = new ArrayList<String>(512);
        try {
            BufferedReader br = new BufferedReader(new FileReader(fileName));

            String line, str;
            boolean insideText = false;

            while ((line = br.readLine()) != null) { // read all lines
                line = line.trim();
                if (line.length() >= 5)
                    if (line.substring(0, 5).equalsIgnoreCase("data_"))
                        ndb -= 1;
                StringBuffer sb = new StringBuffer(line);
                if (sb.length() == 0)
                    continue; // eliminate blank lines
                if (sb.charAt(0) == '#') { // eliminate lines start with '#'
                    if (DEBUG)
                        System.out.println("Ignore (comment): " + line);
                    continue;
                }
                // <TextRegion>
                if (sb.charAt(0) == ';') {
                    if (insideText) {
                        if (DEBUG)
                            System.out.println("Leaving text region.");
                    } else { // insideText == false
                        if (DEBUG)
                            System.out.println("Entering text region.");
                    }
                    insideText = !insideText;
                    continue;
                }
                if (insideText)
                    continue; // ignore
                // </TextRegion>
                if (ndb == 0)
                    al.add(line);
                else if (DEBUG)
                    System.out.println("Ignore: " + line);
            }
            br.close();
        } catch (IOException e) {
            PmodelException ex = new PmodelException();
            ex.message = "Cannot read the CIF file.";
            throw ex;
        }

        if (DEBUG)
            System.out.println("Input Lines: " + al);

        String line, str;

        int count = -1;
        reading: while (++count < al.size()) {
            line = (String) al.get(count);
            StringTokenizer st = new StringTokenizer(line, DELIMITER);
            // StringTokenizer st = new StringTokenizer(line, DELIMITER + "#");
            str = st.nextToken();

            // expand a loop
            if (str.equalsIgnoreCase("loop_")) {
                count = loopExpanderDriver(al, count);
                continue;
            }

            // space-group number from the International Tables
            if (str.equalsIgnoreCase("_symmetry_int_tables_number")
                    || str.equalsIgnoreCase("_space_group_IT_number")) {
                str = st.nextToken();
                sgn = Integer.parseInt(str);
                continue;
            }

            // crystal system
            if (str.equalsIgnoreCase("_symmetry_cell_setting")) {
                str = st.nextToken();
                setCrystalSystem(str);
                continue;
            }

            // Hermann-Mauguin
            if (str.equalsIgnoreCase("_symmetry_space_group_name_H-M")) {
                setHermannMauguin(line);
                continue;
            }

            // cell length
            for (int i = 0; i < 3; ++i)
                if (str.equalsIgnoreCase("_cell_" + tag_cell_length[i])) {
                    str = st.nextToken();
                    cellLength[i] = toDoubleIgnoreESD(str);
                    continue reading;
                }

            // cell angle
            for (int i = 0; i < 3; ++i)
                if (str.equalsIgnoreCase("_cell_" + tag_cell_angle[i])) {
                    str = st.nextToken();
                    cellAngle[i] = toDoubleIgnoreESD(str);
                    continue reading;
                }

            if (DEBUG)
                System.out.println("Ignore: " + line);
        }
    }

    private int loopExpanderDriver(ArrayList al, int count)
            throws PmodelException {
        switch (loopExpanderGetType(al, count)) {
        case ATOM:
            count = loopExpanderAtom(al, count);
            break;
        case OPERATOR:
            count = loopExpanderSymOpe(al, count);
            break;
        case UNDEFINED:
            count = loopExpanderUnDef(al, count);
            break;
        default:
            // ERROR
            PmodelRuntimeException ex = new PmodelRuntimeException();
            throw ex;
        }
        return count;
    }

    private LoopType loopExpanderGetType(ArrayList al, int count) {
        String line, str;

        if (DEBUG)
            System.out
                    .println("Loop Start at line: " + count + " " + al.size());
        while (++count < al.size()) {
            line = (String) al.get(count);
            StringTokenizer st = new StringTokenizer(line, DELIMITER);
            str = st.nextToken();
            if (DEBUG)
                System.out.println("str: " + str);

            if (str.charAt(0) != '_')
                return LoopType.UNDEFINED;

            for (int i = 3; i < 5; ++i)
                if (str.equalsIgnoreCase("_atom_site_" + tag_atom_site[i]))
                    // "_atom_site_label" or "_atom_site_type_symbol"
                    return LoopType.ATOM;

            if (str.equalsIgnoreCase("_symmetry_equiv_pos_as_xyz")
                    || str.equalsIgnoreCase("_space_group_symop_operation_xyz")) {
                return LoopType.OPERATOR;
            }

            if (str.equalsIgnoreCase("_publ_author_name")
                    || str.equalsIgnoreCase("_atom_site_aniso_label")) {
                return LoopType.UNDEFINED;
            }
        }
        return LoopType.UNDEFINED;
    }

    private int loopExpanderAtom(ArrayList al, int count)
            throws PmodelException {
        int atomSymbol[] = { -1, -1 };
        int atomFract[] = { -1, -1, -1 };

        String line, str;

        // <STEP1>
        int numDataName = 0;
        while (++count < al.size()) {
            line = (String) al.get(count);
            StringTokenizer st = new StringTokenizer(line, DELIMITER);
            str = st.nextToken();
            if (DEBUG)
                System.out.println("str: " + str);

            if (str.charAt(0) != '_')
                break;

            int i;
            for (i = 0; i < tag_atom_site.length; ++i)
                if (str.equalsIgnoreCase("_atom_site_" + tag_atom_site[i]))
                    break;
            switch (i) {
            case 0: // "_atom_site_fract_x"
            case 1: // "_atom_site_fract_y"
            case 2: // "_atom_site_fract_z"
                atomFract[i] = numDataName;
                break;
            case 3: // "_atom_site_label"
            case 4: // "_atom_site_type_symbol"
                atomSymbol[i - 3] = numDataName;
                break;
            default:
                ; // ignore
            }

            ++numDataName;
        }
        --count;

        // <STEP2>
        if (atomSymbol[0] == -1)
            atomSymbol[0] = atomSymbol[1];
        else if (atomSymbol[1] == -1)
            atomSymbol[1] = atomSymbol[0];
        if (atomFract[0] == -1) {
            PmodelException ex = new PmodelException();
            ex.message = "Element symbol is not specified.";
            throw ex;
        }

        String[] data = new String[numDataName];
        // <STEP3>
        while (++count < al.size()) {
            if (DEBUG)
                System.out.println("LOOP: ");
            line = (String) al.get(count);
            if (line.equalsIgnoreCase("loop_") || line.charAt(0) == '_')
                break;
            StringTokenizer st;
            st = new StringTokenizer(line, DELIMITER);
            for (int i = 0; i < numDataName; ++i) {
                while (!st.hasMoreTokens())
                    if (++count < al.size()) {
                        if (DEBUG)
                            System.out.println("continuation line.");
                        line = (String) al.get(count);
                        st = new StringTokenizer(line, DELIMITER);
                    } else {
                        PmodelException ex = new PmodelException();
                        ex.message = "unsupported format (atomic position).";
                        throw ex;
                    }
                data[i] = st.nextToken();
            }

            String symbol = data[atomSymbol[0]];
            String alias = data[atomSymbol[1]];
            double[] position = new double[3];
            for (int i = 0; i < 3; ++i)
                position[i] = toDoubleIgnoreESD(data[atomFract[i]]);
            atomList.add(new Atom(symbol, alias, position));
        }
        return --count;
    }

    private int loopExpanderSymOpe(ArrayList al, int count)
            throws PmodelException {
        String line, str;
        int symOpe = -1;

        if (DEBUG)
            System.out
                    .println("Loop Start at line: " + count + " " + al.size());
        // <STEP1>
        int numDataName = 0;
        while (++count < al.size()) {
            line = (String) al.get(count);
            StringTokenizer st = new StringTokenizer(line, DELIMITER);
            str = st.nextToken();
            if (DEBUG)
                System.out.println("str: " + str);

            if (str.charAt(0) != '_')
                break;

            if (str.equalsIgnoreCase("_symmetry_equiv_pos_as_xyz")
                    || str.equalsIgnoreCase("_space_group_symop_operation_xyz")) {
                symOpe = numDataName;
            }
            ++numDataName;
        }
        --count;

        // <STEP2>
        ;

        String[] data = new String[numDataName];
        // <STEP3>
        while (++count < al.size()) {
            if (DEBUG)
                System.out.println("LOOP: ");
            line = (String) al.get(count);
            if (line.equalsIgnoreCase("loop_") || line.charAt(0) == '_') // " loop_"
                break;
            StringTokenizer st;
            int start = line.indexOf("'");
            int end = line.lastIndexOf("'");
            if (start != -1) { // found (not not found)
                str = line.substring(start + 1, end);
            } else if (symOpe == 0) {
                str = line;
            } else if (numDataName > 1) {
                /*
                 * <Example> loop_ _symmetry_equiv_pos_site_id
                 * _symmetry_equiv_pos_as_xyz 1 x,y,z </Example>
                 */
                st = new StringTokenizer(line, DELIMITER);
                for (int i = 0; i < numDataName; ++i)
                    data[i] = st.nextToken();
                str = data[symOpe];
            } else {
                // ERROR
                PmodelException ex = new PmodelException();
                ex.message = "unsupported symmetry format.";
                throw ex;
            }
            symmetryEquivPos.add(str);

            // more error check
            ;
        }
        return --count;
    }

    private int loopExpanderUnDef(ArrayList al, int count) {
        String line;

        if (DEBUG)
            System.out
                    .println("Loop Start at line: " + count + " " + al.size());
        while (++count < al.size()) {
            line = (String) al.get(count);
            if (line.charAt(0) != '_')
                break;
        }

        while (++count < al.size()) {
            line = (String) al.get(count);
            if (line.equalsIgnoreCase("loop_") || line.charAt(0) == '_')
                break;
        }

        return --count;
    }

    private void setCrystalSystem(String str) {
        Pattern p = Pattern.compile("'(.*)'"); // eliminate "'"
        Matcher m = p.matcher(str);

        if (m.find()) {
            System.out
                    .println("Warning : _symmetry_cell_setting is surrouned by \"'\".");
            str = m.group(1);
        }

        if (str.equalsIgnoreCase("triclinic"))
            crystalSystem = CrystalSystem.TRICLINIC;
        else if (str.equalsIgnoreCase("monoclinic"))
            crystalSystem = CrystalSystem.MONOCLINIC;
        else if (str.equalsIgnoreCase("orthorhombic"))
            crystalSystem = CrystalSystem.ORTHORHOMBIC;
        else if (str.equalsIgnoreCase("tetragonal"))
            crystalSystem = CrystalSystem.TETRAGONAL;
        else if (str.equalsIgnoreCase("rhombohedral"))
            crystalSystem = CrystalSystem.RHOMBOHEDRAL;
        else if (str.equalsIgnoreCase("trigonal"))
            crystalSystem = CrystalSystem.TRIGONAL;
        else if (str.equalsIgnoreCase("hexagonal"))
            crystalSystem = CrystalSystem.HEXAGONAL;
        else if (str.equalsIgnoreCase("cubic"))
            crystalSystem = CrystalSystem.CUBIC;
        else
            System.out.println("Unknown crystal system: " + str + "\n"
                    + "Ignore '_system_cell_setting' tag.");

        if (DEBUG)
            System.out.println("\tCell System: " + str + ":" + crystalSystem);
        return;
    }

    private void setHermannMauguin(String line) {
        StringTokenizer st1 = new StringTokenizer(line, DELIMITER);
        st1.nextToken(); // skip "_symmetry_space_group_name_H-M"
        StringTokenizer st2 = new StringTokenizer(line, "'");
        st2.nextToken(); // ignore characters before the first "'"
        if (st2.hasMoreTokens()) {
            HM = st2.nextToken(); // surrounded by "'"
        } else {
            System.out
                    .println("Warning : The element of _symmetry_space_group_name_H-M "
                            + "is not surrounded by \"'\".");
            HM = st1.nextToken();
        }

        if (DEBUG)
            System.out.println("\tHermann-Mauguin: " + HM);
    }

    CrystalSystem getCrystalSystem() {
        return this.crystalSystem;
    }

    private void check() throws PmodelException {
        double temp;
        int i;

        // Error Check
        if (atomList.size() == 0) {
            PmodelException ex = new PmodelException();
            ex.message = "No Atom exist.";
            throw ex;
        }
        temp = 1.0;
        for (i = 0; i < 3; ++i)
            temp *= cellLength[i];
        if (temp == 0.0) {
            PmodelException ex = new PmodelException();
            ex.message = "Bad Cell Length.";
            throw ex;
        }
        temp = 1.0;
        for (i = 0; i < 3; ++i)
            temp *= cellAngle[i];
        if (temp == 0.0) {
            PmodelException ex = new PmodelException();
            ex.message = "Bad Cell Angle.";
            throw ex;
        }
        return;
        // Space Group (will be moved to Pmodel.java)
        /*
         * if (sgn < 1 || sgn > 230) { PmodelException ex = new
         * PmodelException(); throw ex; } if (sgn >= 1 && sgn <= 2) { //
         * TRICLINIC if (cellSystem == -100) cellSystem = 1; if (cellSystem !=1)
         * { PmodelException ex = new PmodelException(); throw ex; } } if (sgn
         * >= 3 && sgn <= 15) { // MONOCLINIC if (cellSystem == -100) cellSystem
         * = 2; if (cellSystem !=2) { PmodelException ex = new
         * PmodelException(); throw ex; } } if (sgn >= 16 && sgn <= 74) { //
         * ORTHORHOMBIC if (cellSystem == -100) cellSystem = 3; if (cellSystem
         * !=3) { PmodelException ex = new PmodelException(); throw ex; } }
         */
    }

    private double toDoubleIgnoreESD(String str) throws PmodelException {
        String r = str.replaceAll("\\(\\d+\\)", ""); // Ignore 'Estimated
                                                     // Standard Deviation'
        return Double.parseDouble(r);
    }
}
