# Maintained by:
# Jim Hester     @jimhester       james.hester@rstudio.com
# Craig Citro    @craigcitro      craigcitro@google.com
#
module Travis
  module Build
    class Script
      class R < Script
        DEFAULTS = {
          # Basic config options
          cran: 'https://cloud.r-project.org',
          repos: {},
          warnings_are_errors: true,
          # Dependencies (installed in this order)
          apt_packages: [],
          brew_packages: [],
          r_binary_packages: [],
          r_packages: [],
          bioc_packages: [],
          r_github_packages: [],
          # Build/test options
          r_build_args: '',
          r_check_args: '--as-cran',
          # Heavy dependencies
          pandoc: true,
          latex: true,
          fortran: true,
          pandoc_version: '2.2',
          # Bioconductor
          bioc: 'https://bioconductor.org/biocLite.R',
          bioc_required: false,
          bioc_use_devel: false,
          disable_homebrew: false,
          r: 'release'
        }

        def initialize(data)
          # TODO(craigcitro): Is there a way to avoid explicitly
          # naming arguments here?
          super
          @devtools_installed = false
          @bioc_installed = false
        end

        def export
          super
          sh.export 'TRAVIS_R_VERSION', r_version, echo: false
          sh.export 'TRAVIS_R_VERSION_STRING', config[:r].to_s, echo: false
          sh.export 'R_LIBS_USER', '~/R/Library', echo: false
          sh.export 'R_LIBS_SITE', '/usr/local/lib/R/site-library:/usr/lib/R/site-library', echo: false
          sh.export '_R_CHECK_CRAN_INCOMING_', 'false', echo: false
          sh.export 'NOT_CRAN', 'true', echo: false
          sh.export 'R_PROFILE', "~/.Rprofile.site", echo: false
        end

        def configure
          super

          sh.echo 'R for Travis-CI is not officially supported, '\
                  'but is community maintained.', ansi: :green
          sh.echo 'Please file any issues at https://github.com/travis-ci/travis-ci/issues'
          sh.echo 'and mention @craigcitro and @jimhester in the issue'

          sh.fold 'R-install' do
            sh.with_options({ assert: true,  echo: true,  timing: true  }) do
              sh.echo 'Installing R', ansi: :yellow
              case config[:os]
              when 'linux'
                # Set up our CRAN mirror.
                sh.cmd 'sudo add-apt-repository '\
                  "\"deb #{repos[:CRAN]}/bin/linux/ubuntu "\
                  "$(lsb_release -cs)/\""
                sh.cmd 'sudo apt-key adv --keyserver keyserver.ubuntu.com '\
                  '--recv-keys E084DAB9'

                # Add marutter's c2d4u repository.
                if r_version_less_than('3.5.0')
                  sh.cmd 'sudo add-apt-repository -y "ppa:marutter/rrutter"'
                  sh.cmd 'sudo add-apt-repository -y "ppa:marutter/c2d4u"'
                else
                  sh.cmd 'sudo add-apt-repository -y "ppa:marutter/rrutter3.5"'
                  sh.cmd 'sudo add-apt-repository -y "ppa:marutter/c2d4u3.5"'
                end

                # Update after adding all repositories. Retry several
                # times to work around flaky connection to Launchpad PPAs.
                sh.cmd 'sudo apt-get update -qq', retry: true

                # Install precompiled R
                # Install only the dependencies for an R development environment except for
                # libpcre3-dev or r-base-core because they will be included in
                # the R binary tarball.
                # Dependencies queried with `apt-cache depends -i r-base-dev`.
                # qpdf and texinfo are also needed for --as-cran # checks:
                # https://stat.ethz.ch/pipermail/r-help//2012-September/335676.html
                optional_apt_pkgs = ""
                optional_apt_pkgs << "gfortran" if config[:fortran]
                sh.cmd 'sudo apt-get install -y --no-install-recommends '\
                  'build-essential gcc g++ libblas-dev liblapack-dev '\
                  'libncurses5-dev libreadline-dev libjpeg-dev '\
                  'libpcre3-dev libpng-dev zlib1g-dev libbz2-dev liblzma-dev libicu-dev '\
                  'cdbs qpdf texinfo libssh2-1-dev '\
                  "#{optional_apt_pkgs}", retry: true

                r_filename = "R-#{r_version}-$(lsb_release -cs).xz"
                r_url = "https://s3.amazonaws.com/rstudio-travis/#{r_filename}"
                sh.cmd "curl -fLo /tmp/#{r_filename} #{r_url}", retry: true
                sh.cmd "tar xJf /tmp/#{r_filename} -C ~"
                sh.export 'PATH', "$HOME/R-bin/bin:$PATH", echo: false
                sh.export 'LD_LIBRARY_PATH', "$HOME/R-bin/lib:$LD_LIBRARY_PATH", echo: false
                sh.rm "/tmp/#{r_filename}"

                sh.cmd "sudo mkdir -p /usr/local/lib/R/site-library $R_LIBS_USER"
                sh.cmd 'sudo chmod 2777 /usr/local/lib/R /usr/local/lib/R/site-library $R_LIBS_USER'
              when 'osx'
                # We want to update, but we don't need the 800+ lines of
                # output.
                sh.cmd 'brew update >/dev/null', retry: true

                # R-devel builds available at research.att.com
                if r_version == 'devel'
                  r_url = "https://r.research.att.com/el-capitan/R-devel/R-devel-el-capitan-signed.pkg"

                # The latest release is the only one available in /bin/macosx
                elsif r_version == r_latest
                  r_url = "#{repos[:CRAN]}/bin/macosx/R-latest.pkg"

                # 3.2.5 was never built for OS X so
                # we need to use 3.2.4-revised, which is the same codebase
                # https://stat.ethz.ch/pipermail/r-devel/2016-May/072642.html
                elsif r_version == '3.2.5'
                  r_url = "#{repos[:CRAN]}/bin/macosx/old/R-3.2.4-revised.pkg"

                # the old archive has moved after 3.4.0
                elsif r_version_less_than('3.4.0')
                  r_url = "#{repos[:CRAN]}/bin/macosx/old/R-#{r_version}.pkg"
                else
                  r_url = "#{repos[:CRAN]}/bin/macosx/el-capitan/base/R-#{r_version}.pkg"
                end

                # Install from latest CRAN binary build for OS X
                sh.cmd "curl -fLo /tmp/R.pkg #{r_url}", retry: true

                sh.echo 'Installing OS X binary package for R'
                sh.cmd 'sudo installer -pkg "/tmp/R.pkg" -target /'
                sh.rm '/tmp/R.pkg'

                setup_fortran_osx if config[:fortran]

              else
                sh.failure "Operating system not supported: #{config[:os]}"
              end

              # Set repos in ~/.Rprofile
              repos_str = repos.collect {|k,v| "#{k} = \"#{v}\""}.join(", ")
              options_repos = "options(repos = c(#{repos_str}))"
              sh.cmd %Q{echo '#{options_repos}' > ~/.Rprofile.site}

              # PDF manual requires latex
              if config[:latex]
                setup_latex
              else
                config[:r_check_args] << " --no-manual"
              end

              setup_bioc if needs_bioc?
              setup_pandoc if config[:pandoc]

              # Removes preinstalled homebrew
              disable_homebrew if config[:disable_homebrew]
            end
          end
        end

        def announce
          super
          sh.fold 'R-session-info' do
            sh.echo 'R session information', ansi: :yellow
            sh.cmd 'Rscript -e \'sessionInfo()\''
          end
        end

        def install
          super

          sh.if '! -e DESCRIPTION' do
            sh.failure "No DESCRIPTION file found, user must supply their own install and script steps"
          end

          sh.fold "R-dependencies" do
            sh.echo 'Installing package dependencies', ansi: :yellow

            # Install any declared packages
            apt_install config[:apt_packages]
            brew_install config[:brew_packages]
            r_binary_install config[:r_binary_packages]
            r_install config[:r_packages]
            r_install config[:bioc_packages]
            r_github_install config[:r_github_packages]

            # Install dependencies for the package we're testing.
            install_deps
          end

          if @devtools_installed
            sh.fold 'R-installed-versions' do
              sh.echo 'Installed package versions', ansi: :yellow
              sh.cmd 'Rscript -e \'devtools::session_info(installed.packages()[, "Package"])\''
            end
          end
        end

        def script
          # Build the package
          sh.if '! -e DESCRIPTION' do
            sh.failure "No DESCRIPTION file found, user must supply their own install and script steps"
          end

          tarball_script =
            '$version = $1 if (/^Version:\s(\S+)/);'\
            '$package = $1 if (/^Package:\s*(\S+)/);'\
            'END { print "${package}_$version.tar.gz" }'\

          sh.export 'PKG_TARBALL', "$(perl -ne '#{tarball_script}' DESCRIPTION)", echo: false
          sh.fold 'R-build' do
            sh.echo 'Building package', ansi: :yellow
            sh.echo "Building with: R CMD build ${R_BUILD_ARGS}"
            sh.cmd "R CMD build #{config[:r_build_args]} .",
                   assert: true
          end

          # Check the package
          sh.fold 'R-check' do
            sh.echo 'Checking package', ansi: :yellow
            # Test the package
            sh.echo 'Checking with: R CMD check "${PKG_TARBALL}" '\
              "#{config[:r_check_args]}"
            sh.cmd "R CMD check \"${PKG_TARBALL}\" #{config[:r_check_args]}; "\
              "CHECK_RET=$?", assert: false
          end
          export_rcheck_dir

          if @devtools_installed
            # Output check summary
            sh.cmd 'Rscript -e "message(devtools::check_failures(path = \"${RCHECK_DIR}\"))"', echo: false
          end

          # Build fails if R CMD check fails
          sh.if '$CHECK_RET -ne 0' do
            dump_error_logs
            sh.failure 'R CMD check failed'
          end

          # Turn warnings into errors, if requested.
          if config[:warnings_are_errors]
            sh.cmd 'grep -q -R "WARNING" "${RCHECK_DIR}/00check.log"', echo: false, assert: false
            sh.if '$? -eq 0' do
              dump_error_logs
              sh.failure "Found warnings, treating as errors (as requested)."
            end
          end

        end

        def setup_cache
          if data.cache?(:packages)
            sh.fold 'package cache' do
              sh.echo 'Setting up package cache', ansi: :yellow
              directory_cache.add '$R_LIBS_USER'
            end
          end
        end

        def cache_slug
          super << '--R-' << r_version
        end

        def use_directory_cache?
          super || data.cache?(:packages)
        end

        private

        def needs_bioc?
          config[:bioc_required] || !config[:bioc_packages].empty?
        end

        def packages_as_arg(packages)
          packages = Array(packages)
          quoted_pkgs = packages.collect{|p| "\"#{p}\""}
          "c(#{quoted_pkgs.join(', ')})"
        end

        def as_r_boolean(bool)
          bool ? "TRUE" : "FALSE"
        end

        def r_install(packages)
          return if packages.empty?
          packages = Array(packages)
          sh.echo "Installing R packages: #{packages.join(', ')}"
          pkg_arg = packages_as_arg(packages)
          install_script =
            "install.packages(#{pkg_arg});"\
            "if (!all(#{pkg_arg} %in% installed.packages())) {"\
            ' q(status = 1, save = "no")'\
            '}'
          sh.cmd "Rscript -e '#{install_script}'"
        end

        def r_github_install(packages)
          return if packages.empty?
          packages = Array(packages)
          setup_devtools
          sh.echo "Installing R packages from GitHub: #{packages.join(', ')}"
          pkg_arg = packages_as_arg(packages)
          install_script = "devtools::install_github(#{pkg_arg}, build_vignettes = FALSE)"
          sh.cmd "Rscript -e '#{install_script}'"
        end

        def r_binary_install(packages)
          return if packages.empty?
          packages = Array(packages)
          if config[:os] == 'linux'
            if !config[:sudo] or config[:dist] == 'precise'
              sh.echo "R binary packages not supported with 'sudo: false' or 'dist: precise', "\
                ' falling back to source install'
              return r_install packages
            end
            sh.echo "Installing *binary* R packages: #{packages.join(', ')}"
            apt_install packages.collect{|p| "r-cran-#{p.downcase}"}
          else
            sh.echo "R binary packages not supported on #{config[:os]}, "\
                    'falling back to source install'
            r_install packages
          end
        end

        def apt_install(packages)
          return if packages.empty?
          packages = Array(packages)
          return unless (config[:os] == 'linux')
          pkg_arg = packages.join(' ')
          sh.echo "Installing apt packages: #{packages.join(', ')}"
          sh.cmd "sudo apt-get install -y #{pkg_arg}", retry: true
        end

        def brew_install(packages)
          return if packages.empty?
          packages = Array(packages)
          return unless (config[:os] == 'osx')
          pkg_arg = packages.join(' ')
          sh.echo "Installing brew packages: #{packages.join(', ')}"
          sh.cmd "brew install #{pkg_arg}", retry: true
        end

        def install_deps
          setup_devtools
          install_script =
            'deps <- devtools::dev_package_deps(dependencies = NA);'\
            'devtools::install_deps(dependencies = TRUE);'\
            'if (!all(deps$package %in% installed.packages())) {'\
            ' message("missing: ", paste(setdiff(deps$package, installed.packages()), collapse=", "));'\
            ' q(status = 1, save = "no")'\
            '}'
          sh.cmd "Rscript -e '#{install_script}'"
        end

        def export_rcheck_dir
          # Simply strip the tarball name until the last _ and add '.Rcheck',
          # relevant R code # https://github.com/wch/r-source/blob/840a972338042b14aa5855cc431b2d0decf68234/src/library/tools/R/check.R#L4608-L4615
          sh.export 'RCHECK_DIR', "$(expr \"$PKG_TARBALL\" : '\\(.*\\)_').Rcheck", echo: false
        end

        def dump_error_logs
          dump_log("fail")
          dump_log("log")
          dump_log("out")
        end

        def dump_log(type)
          sh.fold "#{type} logs" do
            sh.echo "R CMD check #{type} logs", ansi: :yellow
            cmd =
              'for name in '\
              "$(find \"${RCHECK_DIR}\" -type f -name \"*#{type}\");"\
            'do '\
              'echo ">>> Filename: ${name} <<<";'\
              'cat ${name};'\
              'done'
            sh.cmd cmd
          end
        end

        def setup_bioc
          unless @bioc_installed
            sh.fold 'Bioconductor' do
              sh.echo 'Installing Bioconductor', ansi: :yellow
              bioc_install_script =
                "source(\"#{config[:bioc]}\");"\
                'tryCatch('\
                " useDevel(#{as_r_boolean(config[:bioc_use_devel])}),"\
                ' error=function(e) {if (!grepl("already in use", e$message)) {e}}'\
                  ');'\
                  'cat(append = TRUE, file = "~/.Rprofile.site", "options(repos = BiocInstaller::biocinstallRepos());")'
                sh.cmd "Rscript -e '#{bioc_install_script}'", retry: true
            end
          end
          @bioc_installed = true
        end

        def setup_devtools
          unless @devtools_installed
            case config[:os]
            when 'linux'
              # We can't use devtools binaries because R versions < 3.5 are not
              # compatible with R versions >= 3.5
                r_install ['devtools']
            else
              devtools_check = '!requireNamespace("devtools", quietly = TRUE)'
              devtools_install = 'install.packages("devtools")'
              sh.cmd "Rscript -e 'if (#{devtools_check}) #{devtools_install}'",
                     retry: true
            end
          end
          @devtools_installed = true
        end

        def setup_latex
          case config[:os]
          when 'linux'
            texlive_filename = 'texlive.tar.gz'
            texlive_url = 'https://github.com/jimhester/ubuntu-bin/releases/download/latest/texlive.tar.gz'
            sh.cmd "curl -fLo /tmp/#{texlive_filename} #{texlive_url}"
            sh.cmd "tar xzf /tmp/#{texlive_filename} -C ~"
            sh.export 'PATH', "/$HOME/texlive/bin/x86_64-linux:$PATH"
            sh.cmd 'tlmgr update --self', assert: false
          when 'osx'
            # We use basictex due to disk space constraints.
            mactex = 'BasicTeX.pkg'
            # TODO(craigcitro): Confirm that this will route us to the
            # nearest mirror.
            sh.cmd "curl -fLo \"/tmp/#{mactex}\" --retry 3 http://mirror.ctan.org/systems/mac/mactex/"\
                   "#{mactex}"

            sh.echo 'Installing OS X binary package for MacTeX'
            sh.cmd "sudo installer -pkg \"/tmp/#{mactex}\" -target /"
            sh.rm "/tmp/#{mactex}"
            sh.export 'PATH', '/usr/texbin:/Library/TeX/texbin:$PATH'

            sh.cmd 'sudo tlmgr update --self', assert: false

            # Install common packages
            sh.cmd 'sudo tlmgr install inconsolata upquote '\
              'courier courier-scaled helvetic', assert: false
          end
        end

        def setup_pandoc
          case config[:os]
          when 'linux'
            pandoc_filename = "pandoc-#{config[:pandoc_version]}-1-amd64.deb"
            pandoc_url = "https://github.com/jgm/pandoc/releases/download/#{config[:pandoc_version]}/"\
              "#{pandoc_filename}"

            # Download and install pandoc
            sh.cmd "curl -fLo /tmp/#{pandoc_filename} #{pandoc_url}"
            sh.cmd "sudo dpkg -i /tmp/#{pandoc_filename}"

            # Fix any missing dependencies
            sh.cmd "sudo apt-get install -f"

            # Cleanup
            sh.rm "/tmp/#{pandoc_filename}"
          when 'osx'
          
            # Change OS name if requested version is less than 1.19.2.2
            # Name change was introduced in v2.0 of pandoc.
            # c.f. "Build Infrastructure Improvements" section of
            # https://github.com/jgm/pandoc/releases/tag/2.0
            # Lastly, the last binary for macOS before 2.0 is 1.19.2.1
            os_short_name = version_check_less_than("#{config[:pandoc_version]}", "1.19.2.2") ? "macOS" : "osx"
            
            pandoc_filename = "pandoc-#{config[:pandoc_version]}-#{os_short_name}.pkg"
            pandoc_url = "https://github.com/jgm/pandoc/releases/download/#{config[:pandoc_version]}/"\
              "#{pandoc_filename}"

            # Download and install pandoc
            sh.cmd "curl -fLo /tmp/#{pandoc_filename} #{pandoc_url}"
            sh.cmd "sudo installer -pkg \"/tmp/#{pandoc_filename}\" -target /"

            # Cleanup
            sh.rm "/tmp/#{pandoc_filename}"
          end
        end

        # Install gfortran libraries the precompiled binaries are linked to
        def setup_fortran_osx
          return unless (config[:os] == 'osx')
          if r_version_less_than('3.4')
            sh.cmd 'curl -fLo /tmp/gfortran.tar.bz2 http://r.research.att.com/libs/gfortran-4.8.2-darwin13.tar.bz2', retry: true
            sh.cmd 'sudo tar fvxz /tmp/gfortran.tar.bz2 -C /'
            sh.rm '/tmp/gfortran.tar.bz2'
          else
            sh.cmd "curl -fLo /tmp/gfortran61.dmg http://coudert.name/software/gfortran-6.1-ElCapitan.dmg", retry: true
            sh.cmd 'sudo hdiutil attach /tmp/gfortran61.dmg -mountpoint /Volumes/gfortran'
            sh.cmd 'sudo installer -pkg "/Volumes/gfortran/gfortran-6.1-ElCapitan/gfortran.pkg" -target /'
            sh.cmd 'sudo hdiutil detach /Volumes/gfortran'
            sh.rm '/tmp/gfortran61.dmg'
          end
        end

        # Uninstalls the preinstalled homebrew
        # See FAQ: https://docs.brew.sh/FAQ#how-do-i-uninstall-old-versions-of-a-formula
        def disable_homebrew
          return unless (config[:os] == 'osx')
          sh.cmd "curl -fsSOL https://raw.githubusercontent.com/Homebrew/install/master/uninstall"
          sh.cmd "sudo ruby uninstall --force"
          sh.cmd "rm uninstall"
        end
        
        # Abstract out version check
        def version_check_less_than(version_str_new, version_str_old)
            Gem::Version.new(version_str_old) < Gem::Version.new(version_str_new)
        end

        def r_version
          @r_version ||= normalized_r_version
        end

        def r_version_less_than(str)
          return if normalized_r_version == 'devel' # always false (devel is highest version)
          version_check_less_than(str, normalized_r_version)
        end

        def normalized_r_version(v=Array(config[:r]).first.to_s)
          case v
          when 'release' then '3.5.0'
          when 'oldrel' then '3.3.3'
          when '3.0' then '3.0.3'
          when '3.1' then '3.1.3'
          when '3.2' then '3.2.5'
          when '3.3' then '3.3.3'
          when '3.4' then '3.4.4'
          when '3.5' then '3.5'
          when 'bioc-devel'
            config[:bioc_required] = true
            config[:bioc_use_devel] = true
            'devel'
          when 'bioc-release'
            config[:bioc_required] = true
            config[:bioc_use_devel] = false
            config[:r] = 'release'
            normalized_r_version
          else v
          end
        end

        def r_latest
          normalized_r_version('release')
        end

        def repos
          @repos ||= normalized_repos
        end

        # If CRAN is not set in repos set it with cran
        def normalized_repos
          v = config[:repos]
          if not v.has_key?(:CRAN)
            v[:CRAN] = config[:cran]
          end
          # If the version is less than 3.2 we need to use http repositories
          if r_version_less_than('3.2')
            v.each {|_, url| url.sub!(/^https:/, "http:")}
            config[:bioc].sub!(/^https:/, "http:")
          end
          v
        end
      end
    end
  end
end
