#
# Copyright 2012 The Board of Regents of the University of Wisconsin System.
# Contributors: Jason Shao, James McCurdy, Enhai Xie, Adam G.W. Halstead,
# Michael H. Whitney, Nathan DiPiazza, Trey K. Sato and Yury V. Bukhman
#
# This file is part of GCAT.
#
# GCAT is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# GCAT is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with GCAT.  If not, see <http://www.gnu.org/licenses/>.
#
require 'zip'
# require 'zip/zipfilesystem'
require 'fileutils'
include FileUtils

# single-plate timestamp
SECONDS = "1/3600".to_r.to_f
MAX_FILE_SIZE = 10000000.0

class Assay
  
  include ActiveModel::Validations
  include ActiveModel::Conversion
  extend ActiveModel::Naming
  attr_accessor :input_file, :blank_value, :blank_value_input, :start_index, :remove_points, :remove_jumps, :plate_type,
  :plate_dimensions_row, :plate_dimensions_column, :timestamp_format, :growth_threshold, :layout_file,:filename,:content_type, :model, :loess_input, :console_out, :specg_min,
  :specg_max, :totg_min, :totg_max, :lagT_min, :lagT_max,:transformation, :transformation_input
  

  # (1) Validation of input data file
  validates_presence_of :input_file, :message => '- No input file was specified.'
  #Either look for .csv as file extension, or in mime type (content_type). No need to validate if there is not filename
  #validates_format_of :filename, :with => %r{\.(csv|xlsx)$}i, :message => "- You can only upload csv and xlsx files.", :unless => lambda { self.input_file == nil }
  validates_format_of :filename, :with => %r{\.(csv)$}i, :message => "- You can only upload csv files.", :unless => lambda { self.input_file == nil }
  
  def filename
    unless self.input_file.nil?
      self.filename = self.input_file.original_filename
    end
  end
  
  def content_type
    unless self.input_file.nil?
      self.content_type = self.input_file.content_type
    end
  end
  
  # (2)Validation of transformation
  # if user input is chosen, the user should enter a valid Delta value (A Real Number)
  validates_presence_of :transformation_input,  :if => :user_input_r_value?, :message => '- Please Enter Your Delta Value.'
  validates_numericality_of :transformation_input, :if => :user_input_r_value? , :greater_than_or_equal_to => 0 , :message => '- Invalid value for Delta. Please Enter a positive real number.'
  def user_input_r_value?
    transformation == "-1"
  end
  
  # (3) Validation of OD blank value
  # if user input is chosen,, the user should enter a valid OD blank value(A Real Number)
  #validates_inclusion_of :blank_value, :in => %w( default user  ), :message => '- Invalid blank value. Please choose one of options'
  validates_presence_of :blank_value_input,  :if => :user_input?, :message => '- Please Enter Your OD Blank Value.'
  validates_numericality_of :blank_value_input,:if => :user_input?,  :message => '- Invalid OD blank value. Please Enter A Real Number.'
  def user_input?
    blank_value == "user"
  end
  
  
  # (4) Validation of start_index
  # if user does not enter anything, the system uses the default value start_index = 2
  validates_format_of :start_index,:with => /^[0-9 \s]*$/i, :unless => :default_value?,:message => '- Invalid value for start index. Please Enter A Positive Integer Number'
  def default_value?
    start_index == '' 
  end
  
  
  # (5) Validation of remove_points
  # if user does not enter anything, the system uses the default value remove_points = 0, that is an empty list
  #validates_format_of :remove_points,:with => /^[0-9 \,\s]*$/i, :unless => :remove_points_default_value?, :message => '- Please Enter a comma-separated list of points. Example: 2,3,4,5 (Positive Integer Number)'
  validates_format_of :remove_points,:with => /^(\d|\d+\s*,\s*)*$/i, :unless => :remove_points_default_value?, :message => '- Please Enter a comma-separated list of points. Example: 2,3,4,5 (Positive Integer Number)'
  def remove_points_default_value?
    remove_points == '' 
  end
  
  # (6) Validation of growth threshold
  validates_numericality_of :growth_threshold, :message => '- Please enter a number.'
  
  #validate plate dimensions
  #v1 not including custom plate dimensions
  #validates :plate_dimensions_column, :numericality => { :only_integer => true, :greater_than => 0}
  #validates :plate_dimensions_row, :numericality => { :only_integer => true, :greater_than => 0 }
  
  #validate inoculation timepoint
  validates :start_index, :numericality => { :only_integer => true, :greater_than => 0}
  
  
  def initialize(attributes = {})
    attributes.each do |name, value|
      send("#{name}=", value)
    end
  end

  def persisted?  #
    false
  end

  def parse_form_params
    
    # (1) input data file
    
    # (2) transformation. N value (A Real Number)
    if self.transformation == '-1'
      self.transformation =  Float(self.transformation_input)
    else
      self.transformation = self.transformation.to_f
    end

    # Soothing parameter for growth curve model. Applied for Loess model only.
    if self.model == '-1' and self.loess_input != ""
        self.loess_input = Float(self.loess_input)
    end

    # (3) blank value (A Real Number)
    if self.blank_value == 'default'
      self.blank_value = nil
    elsif self.blank_value == 'zero'
      self.blank_value = 0
    else
      self.blank_value = Float(self.blank_value_input)
    end

    # (4) start index (A Positive Integer Number)
    if 
      start_index == ''
      self.start_index = 2
    else  
      self.start_index.gsub(/\s+/, "")  # remove white spaces
      self.start_index = self.start_index.to_i
    end
    
    # (5) remove points [a space-separated list of points. Example: 2 3 4 5 (Positive Integer Number)]
    self.remove_points = self.remove_points.gsub(/\r/,"")  # "Some text with a carriage return \r"
    self.remove_points = self.remove_points.gsub(/\r\n/,"\n") # "Some text with a carriage return \r\n"
    self.remove_points = self.remove_points.gsub(/\s+/, "")  # remove white spaces

    ##collect! calls .to_i on each string in the array and replaces the string with the result of the conversion.
    self.remove_points = self.remove_points.split(',').collect! {|n| n.to_i}

    ## (6) remove jumps (true/false)
    if self.remove_jumps == 1
      self.remove_jumps = true
    else
      self.remove_jumps = false
    end

    ## (7) Heatmap values
    if (self.specg_min != '' && self.specg_max != '')
      self.specg_max = Float(self.specg_max)
      self.specg_min = Float(self.specg_min)
    end
    if (self.totg_min != '' && self.totg_max != '')
      self.totg_min = Float(self.totg_min)
      self.totg_max = Float(self.totg_max)
    end
    if (self.lagT_min != '' && self.lagT_max != '')
      self.lagT_max = Float(self.lagT_max)
      self.lagT_min = Float(self.lagT_min)
    end


############################################################################################

    return self

  end # end of parse_form_params method

  def pad_date(unit)
    unit.to_i < 10 ? "0" + unit.to_s : unit.to_s
  end
    
  def r_calculation

    # uniqueID = Process.pid.to_s + "-" + Time.now.to_i.to_s
    #wanted the date to be easier to extract NWD 2/26/14
    today = Time.now
    
    uniqueID = today.year.to_s + pad_date(today.month) + pad_date(today.day) + "-" + today.to_i.to_s
    # set working directories for uploaded and generated files
    directoryPath = Rails.root + "public/uploadedFiles/" + uniqueID
    outDir = Rails.root + "public/generatedFiles/" + uniqueID
    out_dir_path = outDir.to_s
    
    # make directory and set permission 
    FileUtils.mkdir_p(outDir)
    FileUtils.chmod 0777, outDir
    FileUtils.mkdir_p(directoryPath)
    FileUtils.chmod 0777, directoryPath

    # upload input data file from where it locates into web server via uri/url
    fromfile = self.input_file
    inputfile = directoryPath + fromfile.original_filename
    FileUtils.copy( fromfile.tempfile.path, inputfile )
 
    # upload layout data file from where it locates into web server via uri/url
    unless self.layout_file.nil?
      fromfile = self.layout_file
      layout_file = directoryPath + fromfile.original_filename
      FileUtils.copy( fromfile.tempfile.path, layout_file )
    end

    #do not massive files
    if(File.size(inputfile) > MAX_FILE_SIZE)
      return {:error_message => "Error: File too big. Maximum file size allowed is #{MAX_FILE_SIZE/(10**6)} MB.", :path => inputfile}
    end
    
    # Try to override stdout to redirect the console output.
    $stdout = File.new(out_dir_path + '/console.out', 'w')
    $stdout.sync = true 

    # use web interface parsed parameters to call R function/library via Rinruby  
    R.eval ('library(GCAT)')
    R.assign "out.dir", out_dir_path
    # That is for Single Plate case. Need modification for multiple plate case
    #Use one set.constants call only!
       
      if self.plate_type == 's'
        R.assign 'single.plate', 'T'
        timestamp_format = SECONDS #self.timestamp_format.to_r.to_f
      elsif self.plate_type == 'm'
        R.assign 'single.plate', 'F'
        timestamp_format = ""+self.timestamp_format+""
      end

    
    #################for warringer data###########################################
    # used for custom dims 
    #R.assign 'plate.nrow', 10
    #R.assign 'plate.ncol', 20
    ##############################################################################
    
    R.assign 'time.input', timestamp_format
    #R.eval ("gcat.set.constants(time.format = #{timestamp_format}, plate.nrow = #{self.plate_dimensions_row}, plate.ncol = #{self.plate_dimensions_column})")

    # assign type of file to load.type; ext determines function call by R
    R.assign "load.type", "csv"
    ext = "csv" 


    # (1) input data file
    R.assign "file", inputfile.to_s

    first_rows = ["Well positions", "Destination plate name", "Plate ID"]
    begin
      file_row = ""
      File.open(inputfile) {|f| file_row = f.readline.split(",").first}
      if(first_rows.include?(file_row) == false)
        return {:error_message => "Error: Unknown file format.", :path => inputfile}
      end
    rescue
      #bad encoding try to validate in R
      first_rows.collect! {|r| r.gsub(" ", ".")} # convert to R format
      R.eval("test.out <- read.csv(file)")
      R.eval("first_entry <- names(test.out)[1]")
      if(first_rows.include?(R.first_entry) == false)
        return {:error_message => "Error: Unknown file format.", :path => inputfile}
      end
    end

    # (2) transformation. N value (A Real Number)
    R.assign "add.constant", self.transformation
    # R.assign "add.constant", 0
    # (3) blank value (A Real Number)
    if (self.blank_value == nil)
      R.eval "blank.value <- NULL"
    elsif (self.blank_value == 0)
      R.eval "blank.value <- 0"
    else
      R.assign "blank.value", self.blank_value
    end
    # (4) start index (A Positive Integer Number)
    R.assign "start.index", self.start_index
    # (5) remove points [a space-separated list of points. Example: 2,3,4,5 (Positive Integer Number)]
    R.assign "points.to.remove", self.remove_points
    
    R.assign  "growth.cutoff", self.growth_threshold
    
     if (self.layout_file ==nil)
      R.eval "layout.file <- NULL"
    else
      R.assign "layout.file", layout_file.to_s
    end
    
    #debugger
    #R.assign "plate.nrow", self.plate_dimensions_row
    #R.assign "plate.ncol", self.plate_dimensions_column
    
    ## (6) remove jumps (true/false)
    if (self.remove_jumps == true)
      R.eval "remove.jumps <- T"
    else
      R.eval "remove.jumps <- F"
    end
    # Using growth curve model. By default if this if block
    # is not taken then the Sigmund model is used.
    if (self.model == -1.to_s)
      R.assign 'use.loess', 'T'
      if (self.loess_input != "")
        R.assign 'smooth.param', self.loess_input
      else
        R.assign 'smooth.param', 0.1
      end
      R.assign 'use.linear.param', 'F'
    elsif (self.model == 0.to_s)
      # Currently not in use. May return someday... NWD 9/1
      #R.assign 'use.linear.param', 'T'
      R.assign 'use.linear.param', 'F' #must be false
      R.assign 'use.loess', 'F'
    else
      # Initialize values for growth curve models.
      R.assign 'use.loess', 'F'
      R.assign 'use.linear.param', 'F'
      R.assign 'smooth.param', 0.1 # default value
    end

    ## Heatmap values
    if (self.specg_max != '' && self.specg_min != '')
      R.assign 'specMin', self.specg_min
      R.assign 'specMax', self.specg_max
      R.eval "specRange <- c(specMin, specMax)"
    else
      R.eval 'specRange <- NA'
    end
    if (self.totg_min != '' && self.totg_max != '')
      R.assign 'totMin', self.totg_min
      R.assign 'totMax', self.totg_max
      R.eval "totalRange <- c(totMin, totMax)"
    else
      R.eval 'totalRange <- NA'
    end
    if (self.lagT_min != '' && self.lagT_max != '')
      R.assign 'lagT_min', self.lagT_min
      R.assign 'lagT_max', self.lagT_max
      R.eval "lagRange <- c(lagT_min, lagT_max)"
    else
      R.eval 'lagRange <- NA'
    end
    
    # This block evaluates the files (csv or xlsx, single.plate or multiple.plate)
    R.eval ('R_file_return_value <- gcat.analysis.main(file, single.plate, layout.file, out.dir=out.dir, graphic.dir = out.dir, add.constant, blank.value, 
                                    start.index, growth.cutoff, use.linear.param=use.linear.param, use.loess=use.loess, smooth.param=smooth.param, lagRange = lagRange, 
                                    totalRange = totalRange, specRange = specRange, points.to.remove = points.to.remove, remove.jumps, time.input, plate.nrow = 8, 
                                    plate.ncol = 12, input.skip.lines = 0, multi.column.headers = c("Plate.ID", "Well", "OD", "Time"), single.column.headers = c("","A1"), 
                                    layout.sheet.headers = c("Strain", "Media Definition"), silent = T, verbose = F, return.fit = F, overview.jpgs = T)')
   
    # good file returns a list of file path(length is more than 1), bad file returns error message string(array length = 1)
    print R.R_file_return_value
    
    R.eval ('R_array_return_length <- length(R_file_return_value)')
    unless  R.R_array_return_length == 1
      puts R.R_file_return_value, "\n"
      files = R.R_file_return_value  # returns a list of file path
      status = true
    else
      error_message = R.R_file_return_value
      if(error_message.include? "Error in <remove.points>")
        num_data_points = error_message.split("data has ").last.gsub("\n\n", "")
        error_message = "Invalid 'Points to ignore'. Please select a value in the range (1-#{num_data_points})."
      else
        # debugger
        console_message = error_message
        split_s = error_message.split(":")
        print split_s
        split_s.each {|x| error_message = x}
      end
      return {:error_message => error_message, :path => inputfile, :console_msg => console_message}
    end
    
    # process generated files
    raise "no files generated" if files.empty?
    #search for "_overview.jpg files" from Array of files
    overviewFiles = ""
    pdfFile = ""
    txtFile = ""
    consoleOut = ""
    files_Array_Size = files.size - 1

    for i in 0..files_Array_Size
      if files[i].include? "_overview.jpg"
        overviewFiles = overviewFiles + files[i] +  "\n"
      end

      if files[i].include? "_plots"
        pdfFile = pdfFile + files[i] +  "\n"
      end

      if files[i].include? ".txt"
        txtFile = txtFile + files[i] +  "\n"
      end
    end
 

    unless pdfFile.empty?
      #pdfFile = pdfFile.sub!(Rails.root.to_s + '/public/', '') #sub!(pattern, replacement) will return nil if no substitutions were performed
      pdfFile = pdfFile.gsub(Rails.root.to_s + '/public/', '') #Oct. 04 2011 by Enhai
      pdfFile = pdfFile.gsub(/\r/,"")  # "Some text with a carriage return \r"
      pdfFile = pdfFile.gsub(/\r\n/,"\n") # "Some text with a carriage return \r\n"
      pdfFile = pdfFile.gsub(/\s+/, "")  # remove white spaces
    end

    unless txtFile.empty?
      #txtFile = txtFile.sub!(Rails.root.to_s + '/public/', '')#sub!(pattern, replacement) will return nil if no substitutions were performed
      txtFile = txtFile.gsub(Rails.root.to_s + '/public/', '') #Oct. 04 2011 by Enhai
      txtFile = txtFile.gsub(/\r/,"")  # "Some text with a carriage return \r"
      txtFile = txtFile.gsub(/\r\n/,"\n") # "Some text with a carriage return \r\n"
      txtFile = txtFile.gsub(/\s+/, "")  # remove white spaces
    end

    # build array named overviewFiles that contains "_overview.jpg files"
    overviewFiles = overviewFiles.split("\n")
   
    raise "no overview files generated" if overviewFiles.empty?

    if self.plate_type == 'm'
      zipfile = out_dir_path + "/multiplePlateAnalysis.zip"
    else
      zipfile = out_dir_path + "/singlePlateAnalysis.zip"
    end
    consoleOut = out_dir_path + "/console.out"
    self.console_out = consoleOut
    # create Zip files at current directory
    Zip::File.open(zipfile, Zip::File::CREATE) { |zf|
      files.each{|file| zf.add(File.basename(file), file)}
      #files.each{|file| zf.add(file.sub(out_dir_path + "/", ""), file))}
      #zf.add(File.basename(consoleOut), consoleOut)
    }
    #return results unless error
    #zip files, jpg of overviews, txt file for datagrid, pdf file
    
    
    {:status => status, :overviewFiles => overviewFiles, :zipfile => zipfile, :txtFile => txtFile, :pdfFile => pdfFile, :inputfile => inputfile, :layout_file => layout_file, :model => self.model,
     :consoleout => consoleOut}
  end # end of r_calculation method

  
end # class Assay
