String.prototype.scan = function(regex) {
    if (!regex.global) throw "regex must have 'global' flag set";
    var r = []
    this.replace(regex, function() {
        r.push(Array.prototype.slice.call(arguments, 1, -2));
    });
    return r;
}

function reverseComplement(dna) {
  return /[^ATCG]/.test(dna) ? "Invalid sequence" :
  [...dna.replace(/./g,x=>x=="A"?"T":x=="T"?"A":x=="C"?"G":"C")].reverse().join("")
}

function getFrames(seq){
  var rev        = reverseComplement(seq)
  var seq_length = seq.length
  return { 
    "1" : seq,  "2": seq.substring(1,seq_length-1),  "3": seq.substring(2,seq_length-1),
    "-1": rev, "-2": rev.substring(1,seq_length-1), "-3": rev.substring(2,seq_length-1)
  }
}

function findOrfInFrame(seq,frame,min,len) {
  var orfs    = []
  var tri_nts = seq.match(/.{1,3}/g)
  var start   = ""
  var stop    = ""
  var orf_seq = ""

  var is_neg  = frame < 0
  var neg_offset = {"1" :  1, "2" :  3, "3" : 5}
  var offset     = neg_offset[Math.abs(frame).toString()]

  tri_nts.forEach( function(tri_nt,i){
    // The very end of the sequence
    if  (i == tri_nts.size - 1) {
      stop  = ((i*3)+frame+2)
      if (is_neg) {
        start = (len - parseInt(start) - offset)
        stop  = (len - parseInt(stop)  - offset)
      }
      if ((orf_seq.length + 3) >= min) {
        orfs.push([start.toString(),">#{stop}",orf_seq])
      }
    }
    // Start of an ORF
    if (orf_seq == "" && tri_nt == "ATG") {
      orf_seq  += tri_nt
      start = (i * 3) + frame
    // Not a stop codon
    } else if (codons[tri_nt] != "*" && orf_seq != ""){
      orf_seq  += tri_nt
    // Find a stop codon
    } else if (codons[tri_nt] == "*" && orf_seq != ""){
      if ((orf_seq.length + 3) >= min){
        stop  = ((i*3)+frame+2)
        if (is_neg) {
          start = (len - parseInt(start) - offset)
          stop  = (len - parseInt(stop)  - offset)
        }
        orfs.push([start.toString(),stop.toString(),orf_seq])
        start   = ""
        stop    = ""
        orf_seq = "" 
      }
    }
  });
  return orfs
}

function findOrfs(seq,min) {
  var frames    = getFrames(seq)
  var len       = seq.length
  var all_orfs  = {}
  var count     = 1
  var frames_l  = [1]//,2,3,-1,-2,-3]
  frames_l.forEach(function(frame){
    var is_neg = frame < 0 ? true : false
    var seq    = frames[frame.toString()]
    var orfs   = findOrfInFrame(seq,frame,min,len)
    orfs.forEach(function(orf){
      var orf_info = {
        'start' : orf[0],
        'stop'  : orf[1],
        'strand': frame > 0 ? "+" : "-",
        'seq'   : orf[2].match(/.{1,3}/g).map(codon=>codons[codon]).join(""),
        'bp'    : orf[2].length + 3,
        'aa'    : orf[2].length / 3,
        'frame' : frame,
      };
      all_orfs[`ORF${count}`] = orf_info
      count += 1
    });
  });
  console.log(all_orfs)
  return all_orfs
}
def get_frames(seq)
  rev = seq.gsub(/[ATGC]/, 'A' => 'T', 'T' => 'A', 'G' => 'C', 'C' => 'G').reverse
  { "1":  seq, "2":  seq[1..-1], "3": seq[2..-1],
    "-1": rev, "-2": rev[1..-1], "-3": rev[2..-1] }
end

def find_orf_in_frame(seq,frame,min,len)
  orfs    = []
  tri_nts = seq.scan(/.{3}/)
  start   = stop = seq = ""
  seq     = seq.reverse.tr("ACTG","TGAC")

  is_neg  = frame < 0
  neg_offset = {"1" =>  1, "2" =>  3, "3" => 5}
  offset     = neg_offset[frame.abs().to_s]

  tri_nts.each_with_index do |tri_nt,i|
    if  (i == tri_nts.size - 1)
        stop  = ((i*3)+frame+2)
        start = (len - start.to_i - offset) if is_neg
        stop  = (len - stop.to_i  - offset) if is_neg
      orfs << [start.to_s,">#{stop}",seq] if (seq.length + 3) >= min
      break
    end
    if seq == "" && tri_nt == "ATG"
      # p "ATG"
      seq  += tri_nt
      start = (i * 3) + frame
    elsif (!$codons.select {|k,v| v == "*"}.keys.include?(tri_nt)) && seq != ""
      seq  += tri_nt
    elsif $codons.select {|k,v| v == "*"}.keys.include?(tri_nt)
      print "\n-----\n"
      print seq.scan(/.{3}/).map{|a| $codons[a]}.join()
      if (seq.length + 3) >= min
        stop  = ((i*3)+frame+2)
        start = (len - start.to_i - offset) if is_neg
        stop  = (len - stop.to_i  - offset) if is_neg
        orfs << [start.to_s,stop.to_s,seq]
      end
      start = stop = seq = ""
    end
  end
  orfs
end

def find_orfs(seq,min)
  frames = get_frames(seq)
  len    = seq.length
  all_orfs  = Hash.new()
  count = 1
  # [1,2,3,-1,-2,-3].each do |frame|
  [1].each do |frame|
    is_neg = frame < 0 ? true : false
    seq    = frames[frame.to_s.to_sym]
    orfs   = find_orf_in_frame(seq,frame,min,len)
    orfs.each do |orf|
      orf_info = {
        start:  orf[0],
        stop:   orf[1],
        strand: frame > 0 ? "+" : "-",
        seq:    orf[2].scan(/.{3}/).map{|a| $codons[a]}.join(),
        bp:     orf[2].length + 3,
        aa:     orf[2].length / 3,
        frame:  frame,
      }
      all_orfs["ORF#{count}"] = orf_info
      count += 1
    end
  end
  all_orfs
end