require 'sinatra/base'
require 'sinatra/partials'
require 'rouge'
require 'ginatra/config'
require 'ginatra/errors'
require 'ginatra/logger'
require 'ginatra/helpers'
require 'ginatra/repo'
require 'ginatra/repo_list'
require 'ginatra/repo_stats'

module Ginatra
  # The main application class.
  # Contains all the core application logic and mounted in +config.ru+ file.
  class App < Sinatra::Base
    include Logger
    helpers Helpers, Sinatra::Partials

    configure do
      set :host, Ginatra.config.host
      set :port, Ginatra.config.port
      set :public_folder, "#{settings.root}/../public"
      set :views, "#{settings.root}/../views"
      enable :dump_errors, :logging, :static
    end

    configure :development do
      # Use better errors in development
      require 'better_errors'
      use BetterErrors::Middleware
      BetterErrors.application_root = settings.root

      # Reload modified files in development
      require 'sinatra/reloader'
      register Sinatra::Reloader
      Dir["#{settings.root}/ginatra/*.rb"].each { |file| also_reload file }
    end

    def cache(obj)
      etag obj if settings.production?
    end

    not_found do
      erb :'404', layout: false
    end

    error Ginatra::RepoNotFound, Ginatra::InvalidRef,
          Rugged::OdbError, Rugged::ObjectError, Rugged::InvalidError do
      halt 404, erb(:'404', layout: false)
    end

    error 500 do
      erb :'500', layout: false
    end

    # The root route
    get '/' do
      @repositories = Ginatra::RepoList.list
      erb :index
    end

    # The atom feed of recent commits to a +repo+.
    #
    # This only returns commits to the +master+ branch.
    #
    # @param [String] repo the repository url-sanitised-name
    get '/:repo.atom' do
      @repo = RepoList.find(params[:repo])
      @commits = @repo.commits

      if @commits.empty?
        return ''
      else
        cache "#{@commits.first.oid}/atom"
        content_type 'application/xml'
        erb :atom, layout: false
      end
    end

    # The html page for a +repo+.
    #
    # Shows the most recent commits in a log format.
    #
    # @param [String] repo the repository url-sanitised-name
    get '/:repo/?' do
      @repo = RepoList.find(params[:repo])

      if @repo.branches.none?
        erb :empty_repo
      else
        params[:page] = 1
        params[:ref] = @repo.branch_exists?('master') ? 'master' : @repo.branches.first.name
        @commits = @repo.commits(params[:ref])
        cache "#{@commits.first.oid}/log"
        @next_commits = !@repo.commits(params[:ref], 10, 10).nil?
        erb :log
      end
    end

    # The atom feed of recent commits to a certain branch of a +repo+.
    #
    # @param [String] repo the repository url-sanitised-name
    # @param [String] ref the repository ref
    get '/:repo/:ref.atom' do
      @repo = RepoList.find(params[:repo])
      @commits = @repo.commits(params[:ref])

      if @commits.empty?
        return ''
      else
        cache "#{@commits.first.oid}/atom/ref"
        content_type 'application/xml'
        erb :atom, layout: false
      end
    end

    # The html page for a given +ref+ of a +repo+.
    #
    # Shows the most recent commits in a log format.
    #
    # @param [String] repo the repository url-sanitised-name
    # @param [String] ref the repository ref
    get '/:repo/:ref' do
      @repo = RepoList.find(params[:repo])
      @commits = @repo.commits(params[:ref])
      cache "#{@commits.first.oid}/ref" if @commits.any?
      params[:page] = 1
      @next_commits = !@repo.commits(params[:ref], 10, 10).nil?
      erb :log
    end

    # The html page for a +repo+ stats.
    #
    # Shows information about repository branch.
    #
    # @param [String] repo the repository url-sanitised-name
    # @param [String] ref the repository ref
    get '/:repo/stats/:ref' do
      @repo = RepoList.find(params[:repo])
      @stats = RepoStats.new(@repo, params[:ref])
      erb :stats
    end

    # The patch file for a given commit to a +repo+.
    #
    # @param [String] repo the repository url-sanitised-name
    # @param [String] commit the repository commit
    get '/:repo/commit/:commit.patch' do
      content_type :txt
      repo   = RepoList.find(params[:repo])
      commit = repo.commit(params[:commit])
      cache "#{commit.oid}/patch"
      diff   = commit.parents.first.diff(commit)
      diff.patch
    end

    # The html representation of a commit.
    #
    # @param [String] repo the repository url-sanitised-name
    # @param [String] commit the repository commit
    get '/:repo/commit/:commit' do
      @repo = RepoList.find(params[:repo])
      @commit = @repo.commit(params[:commit])
      cache @commit.oid
      erb :commit
    end

    # The html representation of a tag.
    #
    # @param [String] repo the repository url-sanitised-name
    # @param [String] tag the repository tag
    get '/:repo/tag/:tag' do
      @repo = RepoList.find(params[:repo])
      @commit = @repo.commit_by_tag(params[:tag])
      cache "#{@commit.oid}/tag"
      erb :commit
    end

    # HTML page for a given tree in a given +repo+
    #
    # @param [String] repo the repository url-sanitised-name
    # @param [String] tree the repository tree
    get '/:repo/tree/:tree' do
      @repo = RepoList.find(params[:repo])
      @tree = @repo.find_tree(params[:tree])
      cache @tree.oid

      @path = {
        blob: "#{params[:repo]}/blob/#{params[:tree]}",
        tree: "#{params[:repo]}/tree/#{params[:tree]}"
      }
      erb :tree, layout: !is_pjax?
    end

    # HTML page for a given tree in a given +repo+.
    #
    # This one supports a splat parameter so you can specify a path.
    #
    # @param [String] repo the repository url-sanitised-name
    # @param [String] tree the repository tree
    get '/:repo/tree/:tree/*' do
      @repo = RepoList.find(params[:repo])
      @tree = @repo.find_tree(params[:tree])
      cache "#{@tree.oid}/#{params[:splat].first}"

      @tree.walk(:postorder) do |root, entry|
        @tree = @repo.lookup entry[:oid] if "#{root}#{entry[:name]}" == params[:splat].first
      end

      @path = {
        blob: "#{params[:repo]}/blob/#{params[:tree]}/#{params[:splat].first}",
        tree: "#{params[:repo]}/tree/#{params[:tree]}/#{params[:splat].first}"
      }
      erb :tree, layout: !is_pjax?
    end

    # HTML page for a given blob in a given +repo+
    #
    # @param [String] repo the repository url-sanitised-name
    # @param [String] tree the repository tree
    get '/:repo/blob/:blob' do
      @repo = RepoList.find(params[:repo])
      @tree = @repo.lookup(params[:tree])

      @tree.walk(:postorder) do |root, entry|
        @blob = entry if "#{root}#{entry[:name]}" == params[:splat].first
      end

      cache @blob[:oid]
      erb :blob, layout: !is_pjax?
    end

    # HTML page for a given blob in a given repo.
    #
    # Uses a splat param to specify a blob path.
    #
    # @param [String] repo the repository url-sanitised-name
    # @param [String] tree the repository tree
    get '/:repo/blob/:tree/*' do
      @repo = RepoList.find(params[:repo])
      @tree = @repo.find_tree(params[:tree])

      @tree.walk(:postorder) do |root, entry|
        @blob = entry if "#{root}#{entry[:name]}" == params[:splat].first
      end

      cache "#{@blob[:oid]}/#{@tree.oid}"
      erb :blob, layout: !is_pjax?
    end

    # HTML page for a raw blob contents in a given repo.
    #
    # Uses a splat param to specify a blob path.
    #
    # @param [String] repo the repository url-sanitised-name
    # @param [String] tree the repository tree
    get '/:repo/raw/:tree/*' do
      @repo = RepoList.find(params[:repo])
      @tree = @repo.find_tree(params[:tree])

      @tree.walk(:postorder) do |root, entry|
        @blob = entry if "#{root}#{entry[:name]}" == params[:splat].first
      end

      cache "#{@blob[:oid]}/#{@tree.oid}/raw"
      blob = @repo.find_blob @blob[:oid]
      if blob.binary?
        content_type 'application/octet-stream'
        blob.text
      else
        content_type :txt
        blob.text
      end
    end

    # Pagination route for the commits to a given ref in a +repo+.
    #
    # @param [String] repo the repository url-sanitised-name
    # @param [String] ref the repository ref
    get '/:repo/:ref/page/:page' do
      pass unless params[:page] =~ /\A\d+\z/
      params[:page] = params[:page].to_i
      @repo = RepoList.find(params[:repo])
      @commits = @repo.commits(params[:ref], 10, (params[:page] - 1) * 10)
      cache "#{@commits.first.oid}/page/#{params[:page]}/ref/#{params[:ref]}" if @commits.any?
      @next_commits = !@repo.commits(params[:ref], 10, params[:page] * 10).nil?
      if params[:page] - 1 > 0
        @previous_commits = !@repo.commits(params[:ref], 10, (params[:page] - 1) * 10).empty?
      end
      erb :log
    end

  end # App
end # Ginatra
