// Copyright (C) 2019 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.

package main

import (
	"bytes"
	"fmt"
	"io/ioutil"
	"net/http"
	"path/filepath"
	"strings"
	"sync"
	"time"
)

const (
	urlPrefix   = "https://raw.githubusercontent.com/syncthing/syncthing/"
	httpTimeout = 10 * time.Second
)

type githubSourceCodeLoader struct {
	mut     sync.Mutex
	version string
	cache   map[string]map[string][][]byte // version -> file -> lines
	client  *http.Client
}

func newGithubSourceCodeLoader() *githubSourceCodeLoader {
	return &githubSourceCodeLoader{
		cache:  make(map[string]map[string][][]byte),
		client: &http.Client{Timeout: httpTimeout},
	}
}

func (l *githubSourceCodeLoader) LockWithVersion(version string) {
	l.mut.Lock()
	l.version = version
	if _, ok := l.cache[version]; !ok {
		l.cache[version] = make(map[string][][]byte)
	}
}

func (l *githubSourceCodeLoader) Unlock() {
	l.mut.Unlock()
}

func (l *githubSourceCodeLoader) Load(filename string, line, context int) ([][]byte, int) {
	filename = filepath.ToSlash(filename)
	lines, ok := l.cache[l.version][filename]
	if !ok {
		// Cache whatever we managed to find (or nil if nothing, so we don't try again)
		defer func() {
			l.cache[l.version][filename] = lines
		}()

		knownPrefixes := []string{"/lib/", "/cmd/"}
		var idx int
		for _, pref := range knownPrefixes {
			idx = strings.Index(filename, pref)
			if idx >= 0 {
				break
			}
		}
		if idx == -1 {
			return nil, 0
		}

		url := urlPrefix + l.version + filename[idx:]
		resp, err := l.client.Get(url)

		if err != nil {
			fmt.Println("Loading source:", err)
			return nil, 0
		}
		if resp.StatusCode != http.StatusOK {
			fmt.Println("Loading source:", resp.Status)
			return nil, 0
		}
		data, err := ioutil.ReadAll(resp.Body)
		_ = resp.Body.Close()
		if err != nil {
			fmt.Println("Loading source:", err.Error())
			return nil, 0
		}
		lines = bytes.Split(data, []byte{'\n'})
	}

	return getLineFromLines(lines, line, context)
}

func getLineFromLines(lines [][]byte, line, context int) ([][]byte, int) {
	if lines == nil {
		// cached error from ReadFile: return no lines
		return nil, 0
	}

	line-- // stack trace lines are 1-indexed
	start := line - context
	var idx int
	if start < 0 {
		start = 0
		idx = line
	} else {
		idx = context
	}
	end := line + context + 1
	if line >= len(lines) {
		return nil, 0
	}
	if end > len(lines) {
		end = len(lines)
	}
	return lines[start:end], idx
}
