/// <reference path="../typescript_definitions/index.d.ts" />

///////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////               Read a file, modify the scene                ////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////

function readTraj(trajFile:File, system:System):Promise<string> {
    system.reader = new TrajectoryReader(trajFile, system);
    return system.reader.lookupReader.promise
}

function readJson(jsonFile:File, system:System){ // this still doesn't work for some reason.  It might be a bigger problem tho.
    return parseFileWith(jsonFile, parseJson, [system])
}

function readParFile(parFile:File, system:System) {
    return parseFileWith(parFile, parsePar, [system])
}

// Creates color overlays
function makeLut(data, key, system) {

    let arr = data[key];
    let min = arr[0], max = arr[0];
    
    for(let i =0; i < arr.length;i++)
    {
        if(min >  arr[i]) min = arr[i];
        if(max <= arr[i]) max = arr[i];
    }
   
    if (lut == undefined){
        lut = new THREE.Lut(defaultColormap, 512);
        lut.setMax(max);
        lut.setMin(min); 
    }
    if (max > lut.maxV){
        lut.setMax(max);
        api.removeColorbar();
    }
    if (min < lut.minV){
        lut.setMin(min);
        api.removeColorbar();
    }

    lut.setLegendOn({ 'layout': 'horizontal', 'position': { 'x': 0, 'y': 0, 'z': 0 }, 'dimensions': { 'width': 2, 'height': 12 } }); //create legend
    lut.setLegendLabels({ 'title': key, 'ticks': 5 }); //set up legend format

    //update every system's color map
    const end = system.systemLength();
    for (let j = 0; j < end; j++) { //insert lut colors into lutCols[] to toggle Lut coloring later
        system.lutCols[j] = lut.getColor(Number(system.colormapFile[key][elements.get(system.globalStartId + j).sid]));
    } 
}

// export the current camera position
const exportCam = ()=>{
    const cam = {
        position: camera.position,
        rotation: camera.rotation,
        up: camera.up,
        target: controls.target,
    }
    const camJSON = JSON.stringify(cam);
    makeTextFile("camera.cam", camJSON);
}
// Read a camera export file
const readCamFile = (file:File)=>{
    file.text().then(txt=>{
        const cam = JSON.parse(txt);
        camera.position.set(cam.position.x,cam.position.y,cam.position.z);
        camera.rotation.set(cam.rotation.x,cam.rotation.y,cam.rotation.z);
        camera.up.set(cam.up.x,cam.up.y,cam.up.z);
        controls.target.set(cam.target.x,cam.target.y,cam.target.z);
    })
}

// Highlight sequences found in cadnano or sequence csv
const handleCSV = (file:File)=>{
    // highlight all the sequences complying with the cadnano file
    // or a line by line sequence file 
    const search_func = (system,seq) => {
        system.strands.forEach(strand => {
            strand.search(seq).forEach(match => {
                api.selectElements(match, true);
            });
        });
    };

    const cadnano_line_to_seq = (line)=> line.split(",")[2].replaceAll("?","N").toUpperCase().trim();
    const reg_line = (line)=> line.replaceAll("?","N").toUpperCase().trim();

    //read in a cadnano csv sequence file and highlight them in the scene
    file.text().then(txt=>{
        let lines = txt.split("\n");
        let len = lines.length;
        let start_id = 0;

        //we handle bothe cadnano and just regular lists
        let processor = reg_line;
        if (lines[0].startsWith("Start,End,Sequence,Length,Color")){
            start_id=1;
            processor= cadnano_line_to_seq;
        }
        for(let i=start_id; i<len;i++){
            if(lines[i]){
            let seq = processor(lines[i]);
            console.log(seq)
            systems.forEach(sys => search_func(sys,seq));
            tmpSystems.forEach(sys => search_func(sys,seq));
            }
        }
        render();
            
    });
}

function readForce(forceFile) {
    forceFile.text().then(text=>{
        //{ can be replaced with \n to make sure no parameter is lost
        while(text.indexOf("{")>=0)
            text = text.replace("{","\n");
        // forces can be split by } because everything between {} is one force
        let forceTxt = text.split("}");

        let trap_objs = [];
        forceTxt.forEach((force) =>{
            let lines = force.split('\n');
                //empty lines and empty traps need not be processed as well as comments
            lines = lines.filter((line)=> line !== "" && !line.startsWith("#"));
            if(lines.length == 0) return;

            let trap_obj = {};
            lines.forEach((line)  =>{
                // remove comments
                let com_pos = line.indexOf("#");
                if (com_pos >= 0) line =  line.slice(0, com_pos).trim();
                // another chance an empty line can be encountered. Remove whitespace
                if(line.trim().length == 0) return;
                // split into option name and value
                let options = line.split("=");
                let lft = options[0].trim();
                let rght = options[1].trim();

                // Check if the string represents a list of floats (numbers separated by commas)
                if (rght.includes(',')) {
                    // Split the string by commas and convert each part to a float
                    let floatList = rght.split(',').map(Number);
                    trap_obj[lft] = floatList;
                } else if (/^-?\d+(\.\d+)?$/.test(rght)) {
                    // Check if the entire string is a valid number
                    trap_obj[lft] = parseFloat(rght);
                } else {
                    // Otherwise, treat it as a string
                    trap_obj[lft] = rght;
                }
            });
            if(Object.keys(trap_obj).length > 0)
                trap_objs.push(trap_obj);
        });

        const forceObjs:Force[] = []
        //handle the different traps
        trap_objs.forEach(f=>{
            switch(f.type){                
                case "mutual_trap":
                    let mutTrap = new MutualTrap();
                    mutTrap.setFromParsedJson(f);
                    mutTrap.update();
                    forceObjs.push(mutTrap);
                    break;
                case "skew_trap":
                    let skewTrap = new SkewTrap();
                    skewTrap.setFromParsedJson(f);
                    skewTrap.update();
                    forceObjs.push(skewTrap);
                    break;
                case "repulsion_plane":
                    let repPlane = new RepulsionPlane();
                    repPlane.setFromParsedJson(f);
                    repPlane.update();
                    forceObjs.push(repPlane);
                    break;
                case "attraction_plane":
                    let attrPlane = new AttractionPlane();
                    attrPlane.setFromParsedJson(f);
                    attrPlane.update();
                    forceObjs.push(attrPlane);
                    break;
                default:
                    notify(`External force -${f["type"]}- type not supported yet, feel free to implement in aux_readers.ts and force.ts`);
                    break;
            }
        });

        forceHandler.set(forceObjs);

        render();
    })
}

// Json files can be a lot of things, read them.
function parseJson(json:string, system:System) {
    const data = JSON.parse(json);
    for (var key in data) {
        if (data[key].length == system.systemLength()) { //if json and dat files match/same length
            if (typeof (data[key][0]) == "number") { //we assume that scalars denote a new color map
                system.setColorFile(data);
                makeLut(data, key, system);
                view.coloringMode.set("Overlay");
            }
            if (data[key][0].length == 3) { //we assume that 3D vectors denote motion
                const end = system.systemLength() + system.globalStartId
                for (let i = system.globalStartId; i < end; i++) {
                    const vec = new THREE.Vector3(data[key][i][0], data[key][i][1], data[key][i][2]);
                    const len = vec.length();
                    vec.normalize();
                    const arrowHelper = new THREE.ArrowHelper(vec, elements.get(i).getInstanceParameter3("bbOffsets"), len, 0x000000);
                    arrowHelper.name = i + "disp";
                    scene.add(arrowHelper);
                }
            }
        }
        else if (data[key][0].length == 6) { //draw arbitrary arrows on the scene
            for (let entry of data[key]) {
                const pos = new THREE.Vector3(entry[0], entry[1], entry[2]);
                const vec = new THREE.Vector3(entry[3], entry[4], entry[5]);
                vec.normalize();
                const arrowHelper = new THREE.ArrowHelper(vec, pos, 5 * vec.length(), 0x00000);
                scene.add(arrowHelper);
            }
        }
        else { //if json and dat files do not match, display error message and set filesLen to 2 (not necessary)
            notify(".json and .top files are not compatible.", "alert");
            return;
        }
    }
}

function readSelectFile(reader) {
    if (systems.length > 1) {
        notify("Warning: Selection files select on global ID, not system ID.  There are multiple systems loaded.", 'warning')
    }
    const ids = (reader.result as string).split(' ').map(function(i) {
        return parseInt(i, 10)
    });
    api.selectElementIDs(ids, true)
}

//reads in an anm parameter file and associates it with the last loaded system.
function parsePar(lines, system) {

    lines = lines.split(/[\n]+/g);
    //remove the header
    lines = lines.slice(1)

    const size = lines.length;

    //create an ANM object to allow visualization
    const net = new Network(networks.length, system.getAAMonomers());

    //process connections
    for (let i = 0; i < size; i++) {
        let l = lines[i].split(/\s+/)
        //extract values
        const p = parseInt(l[0]),
            q = parseInt(l[1]),
            eqDist = parseFloat(l[2]),
            type = l[3],
            strength = parseFloat(l[4]);
        
        if (!Number.isInteger(p) || !Number.isInteger(q) || !Number.isFinite(eqDist) || !Number.isFinite(strength)) {
            notify("Cannot read par file, see console for bad line", 'error')
            console.log("Error on par line", i)
            console.log(l)
            return
        }

        // if its a torsional ANM then there are additional parameters on some lines
        let extraParams = []
        if (l.length > 5) {
            for (let i = 5; i < l.length; i++) {
                extraParams.push(l[i])
            }
        }
        if(Number.isInteger(p) && Number.isInteger(q)){
            net.reducedEdges.addEdge(p, q, eqDist, type, strength, extraParams);
        }
    };
    // Create and Fill Vectors
    net.initInstances(net.reducedEdges.total);
    net.initEdges();
    net.fillConnections(); // fills connection array for
    net.networktype = "par";

    net.prepVis(); // Creates Mesh for visualization
    networks.push(net); // Any network added here shows up in UI network selector
    selectednetwork = net.nid; // auto select network just loaded
    view.addNetwork(net.nid);

    notify("Par file read! Turn on visualization in the Protein tab")
}

// reads hydrogen bonding file generated with Chimera
// hbondinfo is then stored in the pdbfiledatasets
function readHBondFile(file) {
    let reader = new FileReader();
    let pdbInfoIndx = pdbFileInfo.length - 1;

    if(pdbInfoIndx == -1){
        notify("Please Load PDB file to associate H-Bond file with");
        return;
    }

    reader.onload = () => {
        let lines = (reader.result as string).split(/[\n]+/g);
        const size = lines.length;
        let hbonds = [];

        //process hbonds
        for (let i = 0; i < size-1; i++) {
            // trims all split items then removes the empty strings
            let l = lines[i].split(/\s+/).map(function(item) {return item.trim()}).filter(n => n);
            if (recongizedProteinResidues.indexOf(l[0]) != -1) { //check that its a protein residue
                //extract values
                const pos1 = l[1].split("."),
                    atm1 = l[2],
                    id2 = l[3],
                    pos2 = l[4].split("."),
                    atm2 = l[5],
                    dist = parseFloat(l[8]);

                if(recongizedProteinResidues.indexOf(id2) != -1) { //bonded to another protein residue
                    // Chain Identifier, residue number
                    let pdbinds1 = [pos1[1], parseInt(pos1[0])];
                    let pdbinds2 = [pos2[1], parseInt(pos2[0])];

                    let hbond = [pdbinds1, pdbinds2];
                    hbonds.push(hbond);
                }
                // can read hbonds using just model identifiers (no chain identifiers)
            } else if (recongizedProteinResidues.indexOf(l[1]) != -1 && recongizedProteinResidues.indexOf(l[5]) != -1) { // residue is second listed indicates hbonds listed from models
                //extract values
                const pos1 = l[0].split(".")[1],
                    atm1 = l[3],
                    id1 = l[2],
                    id2 = l[6],
                    pos2 = l[4].split(".")[1],
                    atm2 = l[7],
                    dist = parseFloat(l[10]);

                let pdbinds1 = [pos1, parseInt(id1)];
                let pdbinds2 = [pos2, parseInt(id2)];

                let hbond = [pdbinds1, pdbinds2];
                hbonds.push(hbond);
            }
        }
        if(hbonds.length == 0) notify("H bond file format is unrecongized");
        pdbFileInfo[pdbInfoIndx].hydrogenBonds = hbonds;
    }
    reader.readAsText(file);
}

// associates massfile with last loaded system (only needed for Generic Sphere Systems)
function readMassFile(reader){
    let lines = (reader.result as string).split(/[\n]+/g);
    let key ={
        indx: [],
        mass: [],
        radius: []
    }

    if(parseInt(lines[0]) > 27){  // subtypes 0-27 taken by dna/protein subtypes
        //remove the header
        lines = lines.slice(1)
        const size = lines.length;
        for (let i = 0; i < size; i++) {
            let l = lines[i].split(/\s+/)
            //extract values
            const p = parseInt(l[0]),
                mass = parseInt(l[1]),
                radius = parseFloat(l[2]);

            if(p > 26){
                key.indx.push(p-27);
                key.mass.push(mass);
                key.radius.push(radius);
            }

        }

        // change all generic sphere radius and mass according to mass file
        let sub, indx, gs;
        systems.forEach(sys => {
            sys.strands.forEach(strand => {
                if(strand.isGS()){
                    let mon = strand.getMonomers();
                    mon.forEach(be => {
                        sub = parseInt(be.type.substring(2))
                        indx = key.indx.indexOf(sub);
                        if(indx == -1){
                            console.log("Subtype " + sub.toString() + " not found in the provided mass file");
                        } else {
                            gs = <GenericSphere>be;
                            gs.updateSize(key.mass[indx], key.radius[indx]);
                        }
                    })
                }
            })
        })


    } else {
        console.log("No GS Masses in file, (no subtype over 27), double check header")
    }
}


function parseDotBracket(input: string): number[] {
    // Converts a dot-bracket string to a list of paired nucleotides.

    const output: number[] = new Array(input.length).fill(-1);
    const parenQueue: number[] = [];
    const squareQueue: number[] = [];
    const curlyQueue: number[] = [];

    for (let i = 0; i < input.trim().length; i++) {
        const c = input[i];
        switch (c) {
            case '.':
                continue;
            case '(':
                parenQueue.push(i);
                break;
            case '[':
                squareQueue.push(i);
                break;
            case '{':
                curlyQueue.push(i);
                break;
            case ')':
                if (parenQueue.length === 0) {
                    throw new Error("Mismatched parentheses in dot-bracket notation.");
                }
                const parenPair = parenQueue.pop();
                output[i] = parenPair;
                output[parenPair] = i;
                break;
            case ']':
                if (squareQueue.length === 0) {
                    throw new Error("Mismatched square brackets in dot-bracket notation.");
                }
                const squarePair = squareQueue.pop();
                output[i] = squarePair;
                output[squarePair] = i;
                break;
            case '}':
                if (curlyQueue.length === 0) {
                    throw new Error("Mismatched curly brackets in dot-bracket notation.");
                }
                const curlyPair = curlyQueue.pop();
                output[i] = curlyPair;
                output[curlyPair] = i;
                break;
            default:
                throw new Error(`Encountered invalid character '${c}' in dot bracket`);
        }
    }

    return output;
}

function readDotBracket(file:File){
    const updateForceHandler = (forces:Force[])=>{
        forceHandler.set(forces)
        render()
    }
    file.text().then(txt=>{
        // let's define the input db format as follows:
        // - each line can be a db string or a sequence
        // - they have to alternate and be separated by a newline
        // - if a line is a sequence, the next line has to be a db string
        // - if a line is a db string, the next line has to be a sequence 
        // - the function will search in all the systems for a strand with the same length and sequence as the sequence line
        // - if it finds one, it will create a trap between the bases according to the db string
        // - if the file contains only 1 line it has to be a db string and the trap will be created for the last system for all strands with the same length as the db string
        // - unless there are selected bases, in which case the trap will be created only for the selected bases

        // preprocess the file
        // strip spaces and newlines
        const forces: PairwiseForce[] = []
        let lines = txt.split("\n").map(s=>s.replace(/\s/g,'')).filter(s=>s.length>0)

        // now let's parse the file
        let file_length = lines.length
        let db_strings = []
        let sequences = []
        
        for (let i = 0; i < file_length; i++){
            if (i % 2 == 0){
                db_strings.push(lines[i])
            } else {
                sequences.push(lines[i])
            }
        }

        // if we have only one line, it has to be a db string
        if (db_strings.length == 1 && sequences.length == 0){
            let db_string = db_strings[0]
            let pairs = parseDotBracket(db_string)

            // we always work with either the last system or the selectedBases
            let to_process;
            // so do we have selected bases ? 
            if (selectedBases.size > 0) {
                // we do and do only on selectedBases
                to_process = [[... selectedBases].sort( (a,b)=> a.id - b.id)]
                if (to_process[0].length != db_string.length)
                    to_process = [] //make sure we have enough bases selected 
            }
            else {
                // we work with the last system
                to_process = systems[systems.length-1].strands.filter(strand => strand.getLength() == db_string.length).map( s=> s.getMonomers())
            }

            to_process.forEach( elements => {
                pairs.forEach( (pair, i) => {
                    if (pair != -1) {
                        // if pair
                        let trap = new MutualTrap()
                        trap.set(elements[i],elements[pair],.09,1.2,1)
                        forces.push(trap)
                    }
                })
            })
            updateForceHandler(forces)
            return
        }

        // check if the file is valid
        if (db_strings.length != sequences.length){
            notify("Invalid dot bracket file format")
            return
        }

        // if we have multiple lines, we have to search the strands for the subsequences
        // and we'll use the first match we find
        
        for (let i = 0; i < sequences.length; i++){
            let to_process = []
            let db_string = db_strings[i]
            let seq = sequences[i]
            let pairs = parseDotBracket(db_string)
            let strands = systems[systems.length-1].strands.forEach( strand => {
                // check if strand is NucleicAcidStrand
                if (! (strand instanceof NucleicAcidStrand)){ return }
                let matches = strand.search(seq);
                if (matches.length > 0){
                    to_process.push(matches[0])
                }
            })
            to_process.forEach( elements => {
                pairs.forEach( (pair, i) => {
                    if (pair != -1) {
                        // if pair
                        let trap = new MutualTrap()
                        trap.set(elements[i],elements[pair],.09,1.2,1)
                        forces.push(trap)
                    }
                })
            })
            updateForceHandler(forces)
        }  
    })
}