package main

import (
	"bytes"
	"compress/gzip"
	"crypto/tls"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"mime/multipart"
	"net/http"
	"net/url"
	"os"
	"path"
	"regexp"
	"runtime"
	"sort"
	"strconv"
	"strings"
	"time"

	"golang.org/x/net/proxy"

	"github.com/asciimoo/wuzz/config"
	"github.com/asciimoo/wuzz/formatter"

	"github.com/alessio/shellescape"
	"github.com/jroimartin/gocui"
	"github.com/mattn/go-runewidth"
	"github.com/nsf/termbox-go"
)

const VERSION = "0.5.0"

const TIMEOUT_DURATION = 5 // in seconds
const WINDOWS_OS = "windows"
const SEARCH_PROMPT = "search> "

const (
	ALL_VIEWS = ""

	URL_VIEW              = "url"
	URL_PARAMS_VIEW       = "get"
	REQUEST_METHOD_VIEW   = "method"
	REQUEST_DATA_VIEW     = "data"
	REQUEST_HEADERS_VIEW  = "headers"
	STATUSLINE_VIEW       = "status-line"
	SEARCH_VIEW           = "search"
	RESPONSE_HEADERS_VIEW = "response-headers"
	RESPONSE_BODY_VIEW    = "response-body"

	SEARCH_PROMPT_VIEW              = "prompt"
	POPUP_VIEW                      = "popup_view"
	AUTOCOMPLETE_VIEW               = "autocomplete_view"
	ERROR_VIEW                      = "error_view"
	HISTORY_VIEW                    = "history"
	SAVE_DIALOG_VIEW                = "save-dialog"
	SAVE_RESPONSE_DIALOG_VIEW       = "save-response-dialog"
	LOAD_REQUEST_DIALOG_VIEW        = "load-request-dialog"
	SAVE_REQUEST_FORMAT_DIALOG_VIEW = "save-request-format-dialog"
	SAVE_REQUEST_DIALOG_VIEW        = "save-request-dialog"
	SAVE_RESULT_VIEW                = "save-result"
	METHOD_LIST_VIEW                = "method-list"
	HELP_VIEW                       = "help"
)

var VIEW_TITLES = map[string]string{
	POPUP_VIEW:                      "Info",
	ERROR_VIEW:                      "Error",
	HISTORY_VIEW:                    "History",
	SAVE_RESPONSE_DIALOG_VIEW:       "Save Response (enter to submit, ctrl+q to cancel)",
	LOAD_REQUEST_DIALOG_VIEW:        "Load Request (enter to submit, ctrl+q to cancel)",
	SAVE_REQUEST_DIALOG_VIEW:        "Save Request (enter to submit, ctrl+q to cancel)",
	SAVE_REQUEST_FORMAT_DIALOG_VIEW: "Choose export format",
	SAVE_RESULT_VIEW:                "Save Result (press enter to close)",
	METHOD_LIST_VIEW:                "Methods",
	HELP_VIEW:                       "Help",
}

type position struct {
	// value = prc * MAX + abs
	pct float32
	abs int
}

type viewPosition struct {
	x0, y0, x1, y1 position
}

var VIEW_POSITIONS = map[string]viewPosition{
	URL_VIEW: {
		position{0.0, 0},
		position{0.0, 0},
		position{1.0, -2},
		position{0.0, 3}},
	URL_PARAMS_VIEW: {
		position{0.0, 0},
		position{0.0, 3},
		position{0.3, 0},
		position{0.25, 0}},
	REQUEST_METHOD_VIEW: {
		position{0.0, 0},
		position{0.25, 0},
		position{0.3, 0},
		position{0.25, 2}},
	REQUEST_DATA_VIEW: {
		position{0.0, 0},
		position{0.25, 2},
		position{0.3, 0},
		position{0.5, 1}},
	REQUEST_HEADERS_VIEW: {
		position{0.0, 0},
		position{0.5, 1},
		position{0.3, 0},
		position{1.0, -3}},
	RESPONSE_HEADERS_VIEW: {
		position{0.3, 0},
		position{0.0, 3},
		position{1.0, -2},
		position{0.25, 2}},
	RESPONSE_BODY_VIEW: {
		position{0.3, 0},
		position{0.25, 2},
		position{1.0, -2},
		position{1.0, -3}},
	STATUSLINE_VIEW: {
		position{0.0, -1},
		position{1.0, -4},
		position{1.0, 0},
		position{1.0, -1}},
	SEARCH_VIEW: {
		position{0.0, 7},
		position{1.0, -3},
		position{1.0, -1},
		position{1.0, -1}},
	ERROR_VIEW: {
		position{0.0, 0},
		position{0.0, 0},
		position{1.0, -2},
		position{1.0, -2}},
	SEARCH_PROMPT_VIEW: {
		position{0.0, -1},
		position{1.0, -3},
		position{0.0, 8},
		position{1.0, -1}},
	POPUP_VIEW: {
		position{0.5, -9999}, // set before usage using len(msg)
		position{0.5, -1},
		position{0.5, -9999}, // set before usage using len(msg)
		position{0.5, 1}},
	AUTOCOMPLETE_VIEW: {
		position{0, -9999},
		position{0, -9999},
		position{0, -9999},
		position{0, -9999}},
}

type viewProperties struct {
	title    string
	frame    bool
	editable bool
	wrap     bool
	editor   gocui.Editor
	text     string
}

var VIEW_PROPERTIES = map[string]viewProperties{
	URL_VIEW: {
		title:    "URL - press F1 for help",
		frame:    true,
		editable: true,
		wrap:     false,
		editor:   &singleLineEditor{&defaultEditor},
	},
	URL_PARAMS_VIEW: {
		title:    "URL params",
		frame:    true,
		editable: true,
		wrap:     false,
		editor:   &defaultEditor,
	},
	REQUEST_METHOD_VIEW: {
		title:    "Method",
		frame:    true,
		editable: true,
		wrap:     false,
		editor:   &singleLineEditor{&defaultEditor},
		text:     DEFAULT_METHOD,
	},
	REQUEST_DATA_VIEW: {
		title:    "Request data (POST/PUT/PATCH)",
		frame:    true,
		editable: true,
		wrap:     false,
		editor:   &defaultEditor,
	},
	REQUEST_HEADERS_VIEW: {
		title:    "Request headers",
		frame:    true,
		editable: true,
		wrap:     false,
		editor: &AutocompleteEditor{&defaultEditor, func(str string) []string {
			return completeFromSlice(str, REQUEST_HEADERS)
		}, []string{}, false},
	},
	RESPONSE_HEADERS_VIEW: {
		title:    "Response headers",
		frame:    true,
		editable: true,
		wrap:     true,
		editor:   nil, // should be set using a.getViewEditor(g)
	},
	RESPONSE_BODY_VIEW: {
		title:    "Response body",
		frame:    true,
		editable: true,
		wrap:     true,
		editor:   nil, // should be set using a.getViewEditor(g)
	},
	SEARCH_VIEW: {
		title:    "",
		frame:    false,
		editable: true,
		wrap:     false,
		editor:   &singleLineEditor{&SearchEditor{&defaultEditor}},
	},
	STATUSLINE_VIEW: {
		title:    "",
		frame:    false,
		editable: false,
		wrap:     false,
		editor:   nil,
		text:     "",
	},
	SEARCH_PROMPT_VIEW: {
		title:    "",
		frame:    false,
		editable: false,
		wrap:     false,
		editor:   nil,
		text:     SEARCH_PROMPT,
	},
	POPUP_VIEW: {
		title:    "Info",
		frame:    true,
		editable: false,
		wrap:     false,
		editor:   nil,
	},
	AUTOCOMPLETE_VIEW: {
		title:    "",
		frame:    false,
		editable: false,
		wrap:     false,
		editor:   nil,
	},
}

var METHODS = []string{
	http.MethodGet,
	http.MethodPost,
	http.MethodPut,
	http.MethodDelete,
	http.MethodPatch,
	http.MethodOptions,
	http.MethodTrace,
	http.MethodConnect,
	http.MethodHead,
}

var EXPORT_FORMATS = []struct {
	name   string
	export func(r Request) []byte
}{
	{
		name:   "JSON",
		export: exportJSON,
	},
	{
		name:   "curl",
		export: exportCurl,
	},
}

const DEFAULT_METHOD = http.MethodGet

var DEFAULT_FORMATTER = &formatter.TextFormatter{}

var CLIENT = &http.Client{
	Timeout: time.Duration(TIMEOUT_DURATION * time.Second),
}
var TRANSPORT = &http.Transport{
	Proxy: http.ProxyFromEnvironment,
}

var VIEWS = []string{
	URL_VIEW,
	URL_PARAMS_VIEW,
	REQUEST_METHOD_VIEW,
	REQUEST_DATA_VIEW,
	REQUEST_HEADERS_VIEW,
	SEARCH_VIEW,
	RESPONSE_HEADERS_VIEW,
	RESPONSE_BODY_VIEW,
}

var TLS_VERSIONS = map[string]uint16{
	"SSL3.0": tls.VersionSSL30,
	"TLS1.0": tls.VersionTLS10,
	"TLS1.1": tls.VersionTLS11,
	"TLS1.2": tls.VersionTLS12,
}

var defaultEditor ViewEditor

const (
	MIN_WIDTH  = 60
	MIN_HEIGHT = 20
)

type Request struct {
	Url             string
	Method          string
	GetParams       string
	Data            string
	Headers         string
	ResponseHeaders string
	RawResponseBody []byte
	ContentType     string
	Duration        time.Duration
	Formatter       formatter.ResponseFormatter
}

type App struct {
	viewIndex    int
	historyIndex int
	currentPopup string
	history      []*Request
	config       *config.Config
	statusLine   *StatusLine
}

type ViewEditor struct {
	app           *App
	g             *gocui.Gui
	backTabEscape bool
	origEditor    gocui.Editor
}

type AutocompleteEditor struct {
	wuzzEditor         *ViewEditor
	completions        func(string) []string
	currentCompletions []string
	isAutocompleting   bool
}

type SearchEditor struct {
	wuzzEditor *ViewEditor
}

// The singleLineEditor removes multi lines capabilities
type singleLineEditor struct {
	wuzzEditor gocui.Editor
}

func init() {
	TRANSPORT.DisableCompression = true
	CLIENT.Transport = TRANSPORT
}

// Editor funcs

func (e *ViewEditor) Edit(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
	// handle back-tab (\033[Z) sequence
	if e.backTabEscape {
		if ch == 'Z' {
			e.app.PrevView(e.g, nil)
			e.backTabEscape = false
			return
		} else {
			e.origEditor.Edit(v, 0, '[', gocui.ModAlt)
		}
	}
	if ch == '[' && mod == gocui.ModAlt {
		e.backTabEscape = true
		return
	}

	// disable infinite down scroll
	if key == gocui.KeyArrowDown && mod == gocui.ModNone {
		_, cY := v.Cursor()
		_, err := v.Line(cY)
		if err != nil {
			return
		}
	}

	e.origEditor.Edit(v, key, ch, mod)
}

var symbolPattern = regexp.MustCompile("[a-zA-Z0-9-]+$")

func getLastSymbol(str string) string {
	return symbolPattern.FindString(str)
}

func completeFromSlice(str string, completions []string) []string {
	completed := []string{}
	if str == "" || strings.TrimRight(str, " \n") != str {
		return completed
	}
	for _, completion := range completions {
		if strings.HasPrefix(completion, str) && str != completion {
			completed = append(completed, completion)
		}
	}
	return completed
}

func (e *AutocompleteEditor) Edit(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
	if key != gocui.KeyEnter {
		e.wuzzEditor.Edit(v, key, ch, mod)
	}

	cx, cy := v.Cursor()
	line, err := v.Line(cy)
	trimmedLine := line[:cx]

	if err != nil {
		e.wuzzEditor.Edit(v, key, ch, mod)
		return
	}

	lastSymbol := getLastSymbol(trimmedLine)
	if key == gocui.KeyEnter && e.isAutocompleting {
		currentCompletion := e.currentCompletions[0]
		shouldDelete := true
		if len(e.currentCompletions) == 1 {
			shouldDelete = false
		}

		if shouldDelete {
			for range lastSymbol {
				v.EditDelete(true)
			}
		}
		for _, char := range currentCompletion {
			v.EditWrite(char)
		}
		closeAutocomplete(e.wuzzEditor.g)
		e.isAutocompleting = false
		return
	} else if key == gocui.KeyEnter {
		e.wuzzEditor.Edit(v, key, ch, mod)
	}

	closeAutocomplete(e.wuzzEditor.g)
	e.isAutocompleting = false

	completions := e.completions(lastSymbol)
	e.currentCompletions = completions

	cx, cy = v.Cursor()
	sx, _ := v.Size()
	ox, oy, _, _, _ := e.wuzzEditor.g.ViewPosition(v.Name())

	maxWidth := sx - cx
	maxHeight := 10

	if len(completions) > 0 {
		comps := completions
		x := ox + cx
		y := oy + cy
		if len(comps) == 1 {
			comps[0] = comps[0][len(lastSymbol):]
		} else {
			y += 1
			x -= len(lastSymbol)
			maxWidth += len(lastSymbol)
		}
		showAutocomplete(comps, x, y, maxWidth, maxHeight, e.wuzzEditor.g)
		e.isAutocompleting = true
	}
}

func (e *SearchEditor) Edit(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
	e.wuzzEditor.Edit(v, key, ch, mod)
	e.wuzzEditor.g.Update(func(g *gocui.Gui) error {
		e.wuzzEditor.app.PrintBody(g)
		return nil
	})
}

// The singleLineEditor removes multi lines capabilities
func (e singleLineEditor) Edit(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
	switch {
	case (ch != 0 || key == gocui.KeySpace) && mod == 0:
		e.wuzzEditor.Edit(v, key, ch, mod)
		// At the end of the line the default gcui editor adds a whitespace
		// Force him to remove
		ox, _ := v.Cursor()
		if ox > 1 && ox >= len(v.Buffer())-2 {
			v.EditDelete(false)
		}
		return
	case key == gocui.KeyEnter:
		return
	case key == gocui.KeyArrowRight:
		ox, _ := v.Cursor()
		if ox >= len(v.Buffer())-1 {
			return
		}
	case key == gocui.KeyHome || key == gocui.KeyArrowUp:
		v.SetCursor(0, 0)
		v.SetOrigin(0, 0)
		return
	case key == gocui.KeyEnd || key == gocui.KeyArrowDown:
		width, _ := v.Size()
		lineWidth := len(v.Buffer()) - 1
		if lineWidth > width {
			v.SetOrigin(lineWidth-width, 0)
			lineWidth = width - 1
		}
		v.SetCursor(lineWidth, 0)
		return
	}
	e.wuzzEditor.Edit(v, key, ch, mod)
}

//

func (a *App) getResponseViewEditor(g *gocui.Gui) gocui.Editor {
	return &ViewEditor{a, g, false, gocui.EditorFunc(func(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) {
		return
	})}
}

func (p position) getCoordinate(max int) int {
	return int(p.pct*float32(max)) + p.abs
}

func setView(g *gocui.Gui, viewName string) (*gocui.View, error) {
	maxX, maxY := g.Size()
	position := VIEW_POSITIONS[viewName]
	return g.SetView(viewName,
		position.x0.getCoordinate(maxX+1),
		position.y0.getCoordinate(maxY+1),
		position.x1.getCoordinate(maxX+1),
		position.y1.getCoordinate(maxY+1))
}

func setViewProperties(v *gocui.View, name string) {
	v.Title = VIEW_PROPERTIES[name].title
	v.Frame = VIEW_PROPERTIES[name].frame
	v.Editable = VIEW_PROPERTIES[name].editable
	v.Wrap = VIEW_PROPERTIES[name].wrap
	v.Editor = VIEW_PROPERTIES[name].editor
	setViewTextAndCursor(v, VIEW_PROPERTIES[name].text)
}

func (a *App) Layout(g *gocui.Gui) error {
	maxX, maxY := g.Size()

	if maxX < MIN_WIDTH || maxY < MIN_HEIGHT {
		if v, err := setView(g, ERROR_VIEW); err != nil {
			if err != gocui.ErrUnknownView {
				return err
			}
			setViewDefaults(v)
			v.Title = VIEW_TITLES[ERROR_VIEW]
			g.Cursor = false
			fmt.Fprintln(v, "Terminal is too small")
		}
		return nil
	}
	if _, err := g.View(ERROR_VIEW); err == nil {
		g.DeleteView(ERROR_VIEW)
		g.Cursor = true
		a.setView(g)
	}

	for _, name := range []string{RESPONSE_HEADERS_VIEW, RESPONSE_BODY_VIEW} {
		vp := VIEW_PROPERTIES[name]
		vp.editor = a.getResponseViewEditor(g)
		VIEW_PROPERTIES[name] = vp
	}

	if a.config.General.DefaultURLScheme != "" && !strings.HasSuffix(a.config.General.DefaultURLScheme, "://") {
		p := VIEW_PROPERTIES[URL_VIEW]
		p.text = a.config.General.DefaultURLScheme + "://"
		VIEW_PROPERTIES[URL_VIEW] = p
	}

	for _, name := range []string{
		URL_VIEW,
		URL_PARAMS_VIEW,
		REQUEST_METHOD_VIEW,
		REQUEST_DATA_VIEW,
		REQUEST_HEADERS_VIEW,
		RESPONSE_HEADERS_VIEW,
		RESPONSE_BODY_VIEW,
		STATUSLINE_VIEW,
		SEARCH_PROMPT_VIEW,
		SEARCH_VIEW,
	} {
		if v, err := setView(g, name); err != nil {
			if err != gocui.ErrUnknownView {
				return err
			}
			setViewProperties(v, name)
		}
	}
	refreshStatusLine(a, g)

	return nil
}

func (a *App) NextView(g *gocui.Gui, v *gocui.View) error {
	a.viewIndex = (a.viewIndex + 1) % len(VIEWS)
	return a.setView(g)
}

func (a *App) PrevView(g *gocui.Gui, v *gocui.View) error {
	a.viewIndex = (a.viewIndex - 1 + len(VIEWS)) % len(VIEWS)
	return a.setView(g)
}

func (a *App) setView(g *gocui.Gui) error {
	a.closePopup(g, a.currentPopup)
	_, err := g.SetCurrentView(VIEWS[a.viewIndex])
	return err
}

func (a *App) setViewByName(g *gocui.Gui, name string) error {
	for i, v := range VIEWS {
		if v == name {
			a.viewIndex = i
			return a.setView(g)
		}
	}
	return fmt.Errorf("View not found")
}

func popup(g *gocui.Gui, msg string) {
	pos := VIEW_POSITIONS[POPUP_VIEW]
	pos.x0.abs = -len(msg)/2 - 1
	pos.x1.abs = len(msg)/2 + 1
	VIEW_POSITIONS[POPUP_VIEW] = pos

	p := VIEW_PROPERTIES[POPUP_VIEW]
	p.text = msg
	VIEW_PROPERTIES[POPUP_VIEW] = p

	if v, err := setView(g, POPUP_VIEW); err != nil {
		if err != gocui.ErrUnknownView {
			return
		}
		setViewProperties(v, POPUP_VIEW)
		g.SetViewOnTop(POPUP_VIEW)
	}
}

func minInt(x, y int) int {
	if x < y {
		return x
	}
	return y
}

func closeAutocomplete(g *gocui.Gui) {
	g.DeleteView(AUTOCOMPLETE_VIEW)
}

func showAutocomplete(completions []string, left, top, maxWidth, maxHeight int, g *gocui.Gui) {
	// Get the width of the widest completion
	completionsWidth := 0
	for _, completion := range completions {
		thisCompletionWidth := len(completion)
		if thisCompletionWidth > completionsWidth {
			completionsWidth = thisCompletionWidth
		}
	}

	// Get the width and height of the autocomplete window
	width := minInt(completionsWidth, maxWidth)
	height := minInt(len(completions), maxHeight)

	newPos := viewPosition{
		x0: position{0, left},
		y0: position{0, top},
		x1: position{0, left + width + 1},
		y1: position{0, top + height + 1},
	}

	VIEW_POSITIONS[AUTOCOMPLETE_VIEW] = newPos

	p := VIEW_PROPERTIES[AUTOCOMPLETE_VIEW]
	p.text = strings.Join(completions, "\n")
	VIEW_PROPERTIES[AUTOCOMPLETE_VIEW] = p

	if v, err := setView(g, AUTOCOMPLETE_VIEW); err != nil {
		if err != gocui.ErrUnknownView {
			return
		}
		setViewProperties(v, AUTOCOMPLETE_VIEW)
		v.BgColor = gocui.ColorBlue
		v.FgColor = gocui.ColorDefault
		g.SetViewOnTop(AUTOCOMPLETE_VIEW)
	}
}

func writeSortedHeaders(output io.Writer, h http.Header) {
	hkeys := make([]string, 0, len(h))
	for hname := range h {
		hkeys = append(hkeys, hname)
	}

	sort.Strings(hkeys)

	for _, hname := range hkeys {
		fmt.Fprintf(output, "\x1b[0;33m%v:\x1b[0;0m %v\n", hname, strings.Join(h[hname], ","))
	}
}

func (a *App) SubmitRequest(g *gocui.Gui, _ *gocui.View) error {
	vrb, _ := g.View(RESPONSE_BODY_VIEW)
	vrb.Clear()
	vrh, _ := g.View(RESPONSE_HEADERS_VIEW)
	vrh.Clear()
	popup(g, "Sending request..")

	var r *Request = &Request{}

	go func(g *gocui.Gui, a *App, r *Request) error {
		defer g.DeleteView(POPUP_VIEW)
		// parse url
		r.Url = getViewValue(g, URL_VIEW)
		u, err := url.Parse(r.Url)
		if err != nil {
			g.Update(func(g *gocui.Gui) error {
				vrb, _ := g.View(RESPONSE_BODY_VIEW)
				fmt.Fprintf(vrb, "URL parse error: %v", err)
				return nil
			})
			return nil
		}

		q, err := url.ParseQuery(strings.Replace(getViewValue(g, URL_PARAMS_VIEW), "\n", "&", -1))
		if err != nil {
			g.Update(func(g *gocui.Gui) error {
				vrb, _ := g.View(RESPONSE_BODY_VIEW)
				fmt.Fprintf(vrb, "Invalid GET parameters: %v", err)
				return nil
			})
			return nil
		}
		originalQuery := u.Query()
		for k, v := range q {
			for _, qp := range v {
				originalQuery.Add(k, qp)
			}
		}
		u.RawQuery = originalQuery.Encode()
		r.GetParams = u.RawQuery

		// parse method
		r.Method = getViewValue(g, REQUEST_METHOD_VIEW)

		// set headers
		headers := http.Header{}
		headers.Set("User-Agent", "")
		r.Headers = getViewValue(g, REQUEST_HEADERS_VIEW)
		for _, header := range strings.Split(r.Headers, "\n") {
			if header != "" {
				header_parts := strings.SplitN(header, ": ", 2)
				if len(header_parts) != 2 {
					g.Update(func(g *gocui.Gui) error {
						vrb, _ := g.View(RESPONSE_BODY_VIEW)
						fmt.Fprintf(vrb, "Invalid header: %v", header)
						return nil
					})
					return nil
				}
				headers.Set(header_parts[0], header_parts[1])
			}
		}

		var body io.Reader

		// parse POST/PUT/PATCH data
		if r.Method == http.MethodPost || r.Method == http.MethodPut || r.Method == http.MethodPatch {
			bodyStr := getViewValue(g, REQUEST_DATA_VIEW)
			r.Data = bodyStr
			if headers.Get("Content-Type") != "multipart/form-data" {
				if headers.Get("Content-Type") == "application/x-www-form-urlencoded" {
					bodyStr = strings.Replace(bodyStr, "\n", "&", -1)
				}
				body = bytes.NewBufferString(bodyStr)
			} else {
				var bodyBytes bytes.Buffer
				multiWriter := multipart.NewWriter(&bodyBytes)
				defer multiWriter.Close()
				postData, err := url.ParseQuery(strings.Replace(getViewValue(g, REQUEST_DATA_VIEW), "\n", "&", -1))
				if err != nil {
					return err
				}
				for postKey, postValues := range postData {
					for i := range postValues {
						if len([]rune(postValues[i])) > 0 && postValues[i][0] == '@' {
							file, err := os.Open(postValues[i][1:])
							if err != nil {
								g.Update(func(g *gocui.Gui) error {
									vrb, _ := g.View(RESPONSE_BODY_VIEW)
									fmt.Fprintf(vrb, "Error: %v", err)
									return nil
								})
								return err
							}
							defer file.Close()
							fw, err := multiWriter.CreateFormFile(postKey, path.Base(postValues[i][1:]))
							if err != nil {
								return err
							}
							if _, err := io.Copy(fw, file); err != nil {
								return err
							}
						} else {
							fw, err := multiWriter.CreateFormField(postKey)
							if err != nil {
								return err
							}
							if _, err := fw.Write([]byte(postValues[i])); err != nil {
								return err
							}
						}
					}
				}
				body = bytes.NewReader(bodyBytes.Bytes())
			}
		}

		// create request
		req, err := http.NewRequest(r.Method, u.String(), body)
		if err != nil {
			g.Update(func(g *gocui.Gui) error {
				vrb, _ := g.View(RESPONSE_BODY_VIEW)
				fmt.Fprintf(vrb, "Request error: %v", err)
				return nil
			})
			return nil
		}
		req.Header = headers

		// set the `Host` header
		if headers.Get("Host") != "" {
			req.Host = headers.Get("Host")
		}

		// do request
		start := time.Now()
		response, err := CLIENT.Do(req)
		r.Duration = time.Since(start)
		if err != nil {
			g.Update(func(g *gocui.Gui) error {
				vrb, _ := g.View(RESPONSE_BODY_VIEW)
				fmt.Fprintf(vrb, "Response error: %v", err)
				return nil
			})
			return nil
		}
		defer response.Body.Close()

		// extract body
		r.ContentType = response.Header.Get("Content-Type")
		if response.Header.Get("Content-Encoding") == "gzip" {
			reader, err := gzip.NewReader(response.Body)
			if err == nil {
				defer reader.Close()
				response.Body = reader
			} else {
				g.Update(func(g *gocui.Gui) error {
					vrb, _ := g.View(RESPONSE_BODY_VIEW)
					fmt.Fprintf(vrb, "Cannot uncompress response: %v", err)
					return nil
				})
				return nil
			}
		}

		bodyBytes, err := ioutil.ReadAll(response.Body)
		if err == nil {
			r.RawResponseBody = bodyBytes
		}

		r.Formatter = formatter.New(a.config, r.ContentType)

		// add to history
		a.history = append(a.history, r)
		a.historyIndex = len(a.history) - 1

		// render response
		g.Update(func(g *gocui.Gui) error {
			vrh, _ := g.View(RESPONSE_HEADERS_VIEW)

			a.PrintBody(g)

			// print status code
			status_color := 32
			if response.StatusCode != 200 {
				status_color = 31
			}
			header := &strings.Builder{}
			fmt.Fprintf(
				header,
				"\x1b[0;%dmHTTP/1.1 %v %v\x1b[0;0m\n",
				status_color,
				response.StatusCode,
				http.StatusText(response.StatusCode),
			)

			writeSortedHeaders(header, response.Header)

			// According to the Go documentation, the Trailer maps trailer
			// keys to values in the same format as Header
			writeSortedHeaders(header, response.Trailer)

			r.ResponseHeaders = header.String()

			fmt.Fprint(vrh, r.ResponseHeaders)
			if _, err := vrh.Line(0); err != nil {
				vrh.SetOrigin(0, 0)
			}

			return nil
		})
		return nil
	}(g, a, r)

	return nil
}

func (a *App) PrintBody(g *gocui.Gui) {
	g.Update(func(g *gocui.Gui) error {
		if len(a.history) == 0 {
			return nil
		}
		req := a.history[a.historyIndex]
		if req.RawResponseBody == nil {
			return nil
		}
		vrb, _ := g.View(RESPONSE_BODY_VIEW)
		vrb.Clear()

		var responseFormatter formatter.ResponseFormatter
		responseFormatter = req.Formatter

		vrb.Title = VIEW_PROPERTIES[vrb.Name()].title + " " + responseFormatter.Title()

		search_text := getViewValue(g, "search")
		if search_text == "" || !responseFormatter.Searchable() {
			err := responseFormatter.Format(vrb, req.RawResponseBody)
			if err != nil {
				fmt.Fprintf(vrb, "Error: cannot decode response body: %v", err)
				return nil
			}
			if _, err := vrb.Line(0); !a.config.General.PreserveScrollPosition || err != nil {
				vrb.SetOrigin(0, 0)
			}
			return nil
		}
		if !a.config.General.ContextSpecificSearch {
			responseFormatter = DEFAULT_FORMATTER
		}
		vrb.SetOrigin(0, 0)
		results, err := responseFormatter.Search(search_text, req.RawResponseBody)
		if err != nil {
			fmt.Fprint(vrb, "Search error: ", err)
			return nil
		}
		if len(results) == 0 {
			vrb.Title = "No results"
			fmt.Fprint(vrb, "Error: no results")
			return nil
		}
		vrb.Title = fmt.Sprintf("%d results", len(results))
		for _, result := range results {
			fmt.Fprintf(vrb, "-----\n%s\n", result)
		}
		return nil
	})
}

func parseKey(k string) (interface{}, gocui.Modifier, error) {
	mod := gocui.ModNone
	if strings.Index(k, "Alt") == 0 {
		mod = gocui.ModAlt
		k = k[3:]
	}
	switch len(k) {
	case 0:
		return 0, 0, errors.New("Empty key string")
	case 1:
		if mod != gocui.ModNone {
			k = strings.ToLower(k)
		}
		return rune(k[0]), mod, nil
	}

	key, found := KEYS[k]
	if !found {
		return 0, 0, fmt.Errorf("Unknown key: %v", k)
	}
	return key, mod, nil
}

func (a *App) setKey(g *gocui.Gui, keyStr, commandStr, viewName string) error {
	if commandStr == "" {
		return nil
	}
	key, mod, err := parseKey(keyStr)
	if err != nil {
		return err
	}
	commandParts := strings.SplitN(commandStr, " ", 2)
	command := commandParts[0]
	var commandArgs string
	if len(commandParts) == 2 {
		commandArgs = commandParts[1]
	}
	keyFnGen, found := COMMANDS[command]
	if !found {
		return fmt.Errorf("Unknown command: %v", command)
	}
	keyFn := keyFnGen(commandArgs, a)
	if err := g.SetKeybinding(viewName, key, mod, keyFn); err != nil {
		return fmt.Errorf("Failed to set key '%v': %v", keyStr, err)
	}
	return nil
}

func (a *App) printViewKeybindings(v io.Writer, viewName string) {
	keys, found := a.config.Keys[viewName]
	if !found {
		return
	}
	mk := make([]string, len(keys))
	i := 0
	for k := range keys {
		mk[i] = k
		i++
	}
	sort.Strings(mk)
	fmt.Fprintf(v, "\n %v\n", viewName)
	for _, key := range mk {
		fmt.Fprintf(v, "  %-15v %v\n", key, keys[key])
	}
}

func (a *App) SetKeys(g *gocui.Gui) error {
	// load config keybindings
	for viewName, keys := range a.config.Keys {
		if viewName == "global" {
			viewName = ALL_VIEWS
		}
		for keyStr, commandStr := range keys {
			if err := a.setKey(g, keyStr, commandStr, viewName); err != nil {
				return err
			}
		}
	}

	g.SetKeybinding(ALL_VIEWS, gocui.KeyF1, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
		if a.currentPopup == HELP_VIEW {
			a.closePopup(g, HELP_VIEW)
			return nil
		}

		help, err := a.CreatePopupView(HELP_VIEW, 60, 40, g)
		if err != nil {
			return err
		}
		help.Title = VIEW_TITLES[HELP_VIEW]
		help.Highlight = false
		fmt.Fprint(help, "Keybindings:\n")
		a.printViewKeybindings(help, "global")
		for _, viewName := range VIEWS {
			if _, found := a.config.Keys[viewName]; !found {
				continue
			}
			a.printViewKeybindings(help, viewName)
		}
		g.SetViewOnTop(HELP_VIEW)
		g.SetCurrentView(HELP_VIEW)
		return nil
	})

	g.SetKeybinding(ALL_VIEWS, gocui.KeyF11, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
		a.config.General.FollowRedirects = !a.config.General.FollowRedirects
		refreshStatusLine(a, g)
		return nil
	})

	g.SetKeybinding(REQUEST_METHOD_VIEW, gocui.KeyEnter, gocui.ModNone, a.ToggleMethodList)

	cursDown := func(g *gocui.Gui, v *gocui.View) error {
		cx, cy := v.Cursor()
		v.SetCursor(cx, cy+1)
		return nil
	}
	cursUp := func(g *gocui.Gui, v *gocui.View) error {
		cx, cy := v.Cursor()
		if cy > 0 {
			cy -= 1
		}
		v.SetCursor(cx, cy)
		return nil
	}
	// history key bindings
	g.SetKeybinding(HISTORY_VIEW, gocui.KeyArrowDown, gocui.ModNone, cursDown)
	g.SetKeybinding(HISTORY_VIEW, gocui.KeyArrowUp, gocui.ModNone, cursUp)
	g.SetKeybinding(HISTORY_VIEW, gocui.KeyEnter, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
		_, cy := v.Cursor()
		// TODO error
		if len(a.history) <= cy {
			return nil
		}
		a.restoreRequest(g, cy)
		return nil
	})

	// method key bindings
	g.SetKeybinding(REQUEST_METHOD_VIEW, gocui.KeyArrowDown, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
		value := strings.TrimSpace(v.Buffer())
		for i, val := range METHODS {
			if val == value && i != len(METHODS)-1 {
				setViewTextAndCursor(v, METHODS[i+1])
			}
		}
		return nil
	})

	g.SetKeybinding(REQUEST_METHOD_VIEW, gocui.KeyArrowUp, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
		value := strings.TrimSpace(v.Buffer())
		for i, val := range METHODS {
			if val == value && i != 0 {
				setViewTextAndCursor(v, METHODS[i-1])
			}
		}
		return nil
	})
	g.SetKeybinding(METHOD_LIST_VIEW, gocui.KeyArrowDown, gocui.ModNone, cursDown)
	g.SetKeybinding(METHOD_LIST_VIEW, gocui.KeyArrowUp, gocui.ModNone, cursUp)
	g.SetKeybinding(METHOD_LIST_VIEW, gocui.KeyEnter, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
		_, cy := v.Cursor()
		v, _ = g.View(REQUEST_METHOD_VIEW)
		setViewTextAndCursor(v, METHODS[cy])
		a.closePopup(g, METHOD_LIST_VIEW)
		return nil
	})
	g.SetKeybinding(SAVE_REQUEST_FORMAT_DIALOG_VIEW, gocui.KeyArrowDown, gocui.ModNone, cursDown)
	g.SetKeybinding(SAVE_REQUEST_FORMAT_DIALOG_VIEW, gocui.KeyArrowUp, gocui.ModNone, cursUp)

	g.SetKeybinding(SAVE_DIALOG_VIEW, gocui.KeyCtrlQ, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
		a.closePopup(g, SAVE_DIALOG_VIEW)
		return nil
	})

	g.SetKeybinding(SAVE_RESULT_VIEW, gocui.KeyEnter, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
		a.closePopup(g, SAVE_RESULT_VIEW)
		return nil
	})
	return nil
}

func (a *App) closePopup(g *gocui.Gui, viewname string) {
	_, err := g.View(viewname)
	if err == nil {
		a.currentPopup = ""
		g.DeleteView(viewname)
		g.SetCurrentView(VIEWS[a.viewIndex%len(VIEWS)])
		g.Cursor = true
	}
}

// CreatePopupView create a popup like view
func (a *App) CreatePopupView(name string, width, height int, g *gocui.Gui) (v *gocui.View, err error) {
	// Remove any concurrent popup
	a.closePopup(g, a.currentPopup)

	g.Cursor = false
	maxX, maxY := g.Size()
	if height > maxY-4 {
		height = maxY - 4
	}
	if width > maxX-4 {
		width = maxX - 4
	}
	v, err = g.SetView(name, maxX/2-width/2-1, maxY/2-height/2-1, maxX/2+width/2, maxY/2+height/2+1)
	if err != nil && err != gocui.ErrUnknownView {
		return
	}
	err = nil
	v.Wrap = false
	v.Frame = true
	v.Highlight = true
	v.SelFgColor = gocui.ColorYellow
	v.SelBgColor = gocui.ColorDefault
	a.currentPopup = name
	return
}

func (a *App) LoadRequest(g *gocui.Gui, loadLocation string) (err error) {
	requestJson, ioErr := ioutil.ReadFile(loadLocation)
	if ioErr != nil {
		g.Update(func(g *gocui.Gui) error {
			vrb, _ := g.View(RESPONSE_BODY_VIEW)
			vrb.Clear()
			fmt.Fprintf(vrb, "File reading error: %v", ioErr)
			return nil
		})
		return nil
	}

	var requestMap map[string]string
	jsonErr := json.Unmarshal(requestJson, &requestMap)
	if jsonErr != nil {
		g.Update(func(g *gocui.Gui) error {
			vrb, _ := g.View(RESPONSE_BODY_VIEW)
			vrb.Clear()
			fmt.Fprintf(vrb, "JSON decoding error: %v", jsonErr)
			return nil
		})
		return nil
	}

	var v *gocui.View
	url, exists := requestMap[URL_VIEW]
	if exists {
		v, _ = g.View(URL_VIEW)
		setViewTextAndCursor(v, url)
	}

	method, exists := requestMap[REQUEST_METHOD_VIEW]
	if exists {
		v, _ = g.View(REQUEST_METHOD_VIEW)
		setViewTextAndCursor(v, method)
	}

	params, exists := requestMap[URL_PARAMS_VIEW]
	if exists {
		v, _ = g.View(URL_PARAMS_VIEW)
		setViewTextAndCursor(v, params)
	}

	data, exists := requestMap[REQUEST_DATA_VIEW]
	if exists {
		g.Update(func(g *gocui.Gui) error {
			v, _ = g.View(REQUEST_DATA_VIEW)
			v.Clear()
			fmt.Fprintf(v, "%v", data)
			return nil
		})
	}

	headers, exists := requestMap[REQUEST_HEADERS_VIEW]
	if exists {
		v, _ = g.View(REQUEST_HEADERS_VIEW)
		setViewTextAndCursor(v, headers)
	}
	return nil
}

func (a *App) ToggleHistory(g *gocui.Gui, _ *gocui.View) (err error) {
	// Destroy if present
	if a.currentPopup == HISTORY_VIEW {
		a.closePopup(g, HISTORY_VIEW)
		return
	}

	history, err := a.CreatePopupView(HISTORY_VIEW, 100, len(a.history), g)
	if err != nil {
		return
	}

	history.Title = VIEW_TITLES[HISTORY_VIEW]

	if len(a.history) == 0 {
		setViewTextAndCursor(history, "[!] No items in history")
		return
	}
	for i, r := range a.history {
		req_str := fmt.Sprintf("[%02d] %v %v", i, r.Method, r.Url)
		if r.GetParams != "" {
			req_str += fmt.Sprintf("?%v", strings.Replace(r.GetParams, "\n", "&", -1))
		}
		if r.Data != "" {
			req_str += fmt.Sprintf(" %v", strings.Replace(r.Data, "\n", "&", -1))
		}
		if r.Headers != "" {
			req_str += fmt.Sprintf(" %v", strings.Replace(r.Headers, "\n", ";", -1))
		}
		fmt.Fprintln(history, req_str)
	}
	g.SetViewOnTop(HISTORY_VIEW)
	g.SetCurrentView(HISTORY_VIEW)
	history.SetCursor(0, a.historyIndex)
	return
}

func (a *App) SaveRequest(g *gocui.Gui, _ *gocui.View) (err error) {
	// Destroy if present
	if a.currentPopup == SAVE_REQUEST_FORMAT_DIALOG_VIEW {
		a.closePopup(g, SAVE_REQUEST_FORMAT_DIALOG_VIEW)
		return
	}
	// Create the view listing the possible formats
	popup, err := a.CreatePopupView(SAVE_REQUEST_FORMAT_DIALOG_VIEW, 30, len(EXPORT_FORMATS), g)
	if err != nil {
		return err
	}

	popup.Title = VIEW_TITLES[SAVE_REQUEST_FORMAT_DIALOG_VIEW]

	// Populate the popup witht the available formats
	for _, r := range EXPORT_FORMATS {
		fmt.Fprintln(popup, r.name)
	}

	g.SetViewOnTop(SAVE_REQUEST_FORMAT_DIALOG_VIEW)
	g.SetCurrentView(SAVE_REQUEST_FORMAT_DIALOG_VIEW)
	popup.SetCursor(0, 0)

	// Bind the enter key, when the format is chosen, save the choice and open
	// the save popup
	g.SetKeybinding(SAVE_REQUEST_FORMAT_DIALOG_VIEW, gocui.KeyEnter, gocui.ModNone, func(g *gocui.Gui, v *gocui.View) error {
		// Save the format index
		_, format := v.Cursor()
		// Open the Save popup
		return a.OpenSaveDialog(VIEW_TITLES[SAVE_REQUEST_DIALOG_VIEW], g,
			func(g *gocui.Gui, _ *gocui.View) error {
				defer a.closePopup(g, SAVE_DIALOG_VIEW)
				saveLocation := getViewValue(g, SAVE_DIALOG_VIEW)

				r := Request{
					Url:       getViewValue(g, URL_VIEW),
					Method:    getViewValue(g, REQUEST_METHOD_VIEW),
					GetParams: getViewValue(g, URL_PARAMS_VIEW),
					Data:      getViewValue(g, REQUEST_DATA_VIEW),
					Headers:   getViewValue(g, REQUEST_HEADERS_VIEW),
				}

				// Export the request using the chosent format
				request := EXPORT_FORMATS[format].export(r)

				// Write the file
				ioerr := ioutil.WriteFile(saveLocation, []byte(request), 0644)

				saveResult := fmt.Sprintf("Request saved successfully in %s", EXPORT_FORMATS[format].name)
				if ioerr != nil {
					saveResult = "Error saving request: " + ioerr.Error()
				}
				viewErr := a.OpenSaveResultView(saveResult, g)

				return viewErr
			},
		)
	})

	return
}

func (a *App) ToggleMethodList(g *gocui.Gui, _ *gocui.View) (err error) {
	// Destroy if present
	if a.currentPopup == METHOD_LIST_VIEW {
		a.closePopup(g, METHOD_LIST_VIEW)
		return
	}

	method, err := a.CreatePopupView(METHOD_LIST_VIEW, 50, len(METHODS), g)
	if err != nil {
		return
	}
	method.Title = VIEW_TITLES[METHOD_LIST_VIEW]

	cur := getViewValue(g, REQUEST_METHOD_VIEW)

	for i, r := range METHODS {
		fmt.Fprintln(method, r)
		if cur == r {
			method.SetCursor(0, i)
		}
	}
	g.SetViewOnTop(METHOD_LIST_VIEW)
	g.SetCurrentView(METHOD_LIST_VIEW)
	return
}

func (a *App) OpenSaveDialog(title string, g *gocui.Gui, save func(g *gocui.Gui, v *gocui.View) error) error {
	dialog, err := a.CreatePopupView(SAVE_DIALOG_VIEW, 60, 1, g)
	if err != nil {
		return err
	}
	g.Cursor = true

	dialog.Title = title
	dialog.Editable = true
	dialog.Wrap = false

	currentDir, err := os.Getwd()
	if err != nil {
		currentDir = ""
	}
	currentDir += "/"

	setViewTextAndCursor(dialog, currentDir)

	g.SetViewOnTop(SAVE_DIALOG_VIEW)
	g.SetCurrentView(SAVE_DIALOG_VIEW)
	dialog.SetCursor(0, len(currentDir))
	g.DeleteKeybinding(SAVE_DIALOG_VIEW, gocui.KeyEnter, gocui.ModNone)
	g.SetKeybinding(SAVE_DIALOG_VIEW, gocui.KeyEnter, gocui.ModNone, save)
	return nil
}

func (a *App) OpenSaveResultView(saveResult string, g *gocui.Gui) (err error) {
	popupTitle := VIEW_TITLES[SAVE_RESULT_VIEW]
	saveResHeight := 1
	saveResWidth := len(saveResult) + 1
	if len(popupTitle)+2 > saveResWidth {
		saveResWidth = len(popupTitle) + 2
	}
	maxX, _ := g.Size()
	if saveResWidth > maxX {
		saveResHeight = saveResWidth/maxX + 1
		saveResWidth = maxX
	}

	saveResultPopup, err := a.CreatePopupView(SAVE_RESULT_VIEW, saveResWidth, saveResHeight, g)
	saveResultPopup.Title = popupTitle
	setViewTextAndCursor(saveResultPopup, saveResult)
	g.SetViewOnTop(SAVE_RESULT_VIEW)
	g.SetCurrentView(SAVE_RESULT_VIEW)
	return err
}

func (a *App) restoreRequest(g *gocui.Gui, idx int) {
	if idx < 0 || idx >= len(a.history) {
		return
	}
	a.closePopup(g, HISTORY_VIEW)
	a.historyIndex = idx
	r := a.history[idx]

	v, _ := g.View(URL_VIEW)
	setViewTextAndCursor(v, r.Url)

	v, _ = g.View(REQUEST_METHOD_VIEW)
	setViewTextAndCursor(v, r.Method)

	v, _ = g.View(URL_PARAMS_VIEW)
	setViewTextAndCursor(v, r.GetParams)

	v, _ = g.View(REQUEST_DATA_VIEW)
	setViewTextAndCursor(v, r.Data)

	v, _ = g.View(REQUEST_HEADERS_VIEW)
	setViewTextAndCursor(v, r.Headers)

	v, _ = g.View(RESPONSE_HEADERS_VIEW)
	setViewTextAndCursor(v, r.ResponseHeaders)

	a.PrintBody(g)

}

func (a *App) LoadConfig(configPath string) error {
	if configPath == "" {
		// Load config from default path
		configPath = config.GetDefaultConfigLocation()
	}

	// If the config file doesn't exist, load the default config
	if _, err := os.Stat(configPath); os.IsNotExist(err) {
		a.config = &config.DefaultConfig
		a.config.Keys = config.DefaultKeys
		a.statusLine, _ = NewStatusLine(a.config.General.StatusLine)
		return nil
	}

	conf, err := config.LoadConfig(configPath)
	if err != nil {
		a.config = &config.DefaultConfig
		a.config.Keys = config.DefaultKeys
		return err
	}

	a.config = conf
	sl, err := NewStatusLine(conf.General.StatusLine)
	if err != nil {
		a.config = &config.DefaultConfig
		a.config.Keys = config.DefaultKeys
		return err
	}
	a.statusLine = sl
	return nil
}

func (a *App) ParseArgs(g *gocui.Gui, args []string) error {
	a.Layout(g)
	g.SetCurrentView(VIEWS[a.viewIndex])
	vheader, err := g.View(REQUEST_HEADERS_VIEW)
	if err != nil {
		return errors.New("Too small screen")
	}
	vheader.Clear()
	vget, _ := g.View(URL_PARAMS_VIEW)
	vget.Clear()
	content_type := ""
	set_data := false
	set_method := false
	set_binary_data := false
	arg_index := 1
	args_len := len(args)
	accept_types := make([]string, 0, 8)
	var body_data []string
	for arg_index < args_len {
		arg := args[arg_index]
		switch arg {
		case "-H", "--header":
			if arg_index == args_len-1 {
				return errors.New("No header value specified")
			}
			arg_index += 1
			header := args[arg_index]
			fmt.Fprintf(vheader, "%v\n", header)
		case "-d", "--data", "--data-binary", "--data-urlencode":
			if arg_index == args_len-1 {
				return errors.New("No POST/PUT/PATCH value specified")
			}

			arg_index += 1
			set_data = true
			set_binary_data = arg == "--data-binary"
			arg_data := args[arg_index]

			if !set_binary_data {
				content_type = "form"
			}

			if arg == "--data-urlencode" {
				// TODO: Replace with `url.PathEscape(..)` in Go 1.8
				arg_data_url := &url.URL{Path: arg_data}
				arg_data = arg_data_url.String()
			}

			body_data = append(body_data, arg_data)
		case "-j", "--json":
			if arg_index == args_len-1 {
				return errors.New("No POST/PUT/PATCH value specified")
			}

			arg_index += 1
			json_str := args[arg_index]
			content_type = "json"
			accept_types = append(accept_types, config.ContentTypes["json"])
			set_data = true
			vdata, _ := g.View(REQUEST_DATA_VIEW)
			setViewTextAndCursor(vdata, json_str)
		case "-X", "--request":
			if arg_index == args_len-1 {
				return errors.New("No HTTP method specified")
			}
			arg_index++
			set_method = true
			method := args[arg_index]
			if content_type == "" && (method == http.MethodPost || method == http.MethodPut || method == http.MethodPatch) {
				content_type = "form"
			}
			vmethod, _ := g.View(REQUEST_METHOD_VIEW)
			setViewTextAndCursor(vmethod, method)
		case "-t", "--timeout":
			if arg_index == args_len-1 {
				return errors.New("No timeout value specified")
			}
			arg_index += 1
			timeout, err := strconv.Atoi(args[arg_index])
			if err != nil || timeout <= 0 {
				return errors.New("Invalid timeout value")
			}
			a.config.General.Timeout = config.Duration{Duration: time.Duration(timeout) * time.Millisecond}
		case "--compressed":
			vh, _ := g.View(REQUEST_HEADERS_VIEW)
			if !strings.Contains(getViewValue(g, REQUEST_HEADERS_VIEW), "Accept-Encoding") {
				fmt.Fprintln(vh, "Accept-Encoding: gzip, deflate")
			}
		case "-e", "--editor":
			if arg_index == args_len-1 {
				return errors.New("No timeout value specified")
			}
			arg_index += 1
			a.config.General.Editor = args[arg_index]
		case "-k", "--insecure":
			a.config.General.Insecure = true
		case "-R", "--disable-redirects":
			a.config.General.FollowRedirects = false
		case "--tlsv1.0":
			a.config.General.TLSVersionMin = tls.VersionTLS10
			a.config.General.TLSVersionMax = tls.VersionTLS10
		case "--tlsv1.1":
			a.config.General.TLSVersionMin = tls.VersionTLS11
			a.config.General.TLSVersionMax = tls.VersionTLS11
		case "--tlsv1.2":
			a.config.General.TLSVersionMin = tls.VersionTLS12
			a.config.General.TLSVersionMax = tls.VersionTLS12
		case "-1", "--tlsv1":
			a.config.General.TLSVersionMin = tls.VersionTLS10
			a.config.General.TLSVersionMax = tls.VersionTLS12
		case "-T", "--tls":
			if arg_index >= args_len-1 {
				return errors.New("Missing TLS version range: MIN,MAX")
			}
			arg_index++
			arg := args[arg_index]
			v := strings.Split(arg, ",")
			min := v[0]
			max := min
			if len(v) > 1 {
				max = v[1]
			}
			minV, minFound := TLS_VERSIONS[min]
			if !minFound {
				return errors.New("Minimum TLS version not found: " + min)
			}
			maxV, maxFound := TLS_VERSIONS[max]
			if !maxFound {
				return errors.New("Maximum TLS version not found: " + max)
			}
			a.config.General.TLSVersionMin = minV
			a.config.General.TLSVersionMax = maxV
		case "-x", "--proxy":
			if arg_index == args_len-1 {
				return errors.New("Missing proxy URL")
			}
			arg_index += 1
			u, err := url.Parse(args[arg_index])
			if err != nil {
				return fmt.Errorf("Invalid proxy URL: %v", err)
			}
			switch u.Scheme {
			case "", "http", "https":
				TRANSPORT.Proxy = http.ProxyURL(u)
			case "socks", "socks5":
				dialer, err := proxy.SOCKS5("tcp", u.Host, nil, proxy.Direct)
				if err != nil {
					return fmt.Errorf("Can't connect to proxy: %v", err)
				}
				TRANSPORT.Dial = dialer.Dial
			default:
				return errors.New("Unknown proxy protocol")
			}
		case "-F", "--form":
			if arg_index == args_len-1 {
				return errors.New("No POST/PUT/PATCH value specified")
			}

			arg_index += 1
			form_str := args[arg_index]
			content_type = "multipart"
			set_data = true
			vdata, _ := g.View(REQUEST_DATA_VIEW)
			setViewTextAndCursor(vdata, form_str)
		case "-f", "--file":
			if arg_index == args_len-1 {
				return errors.New("-f or --file requires a file path be provided as an argument")
			}
			arg_index += 1
			loadLocation := args[arg_index]
			a.LoadRequest(g, loadLocation)
		default:
			u := args[arg_index]
			if strings.Index(u, "http://") != 0 && strings.Index(u, "https://") != 0 {
				u = fmt.Sprintf("%v://%v", a.config.General.DefaultURLScheme, u)
			}
			parsed_url, err := url.Parse(u)
			if err != nil || parsed_url.Host == "" {
				return errors.New("Invalid url")
			}
			if parsed_url.Path == "" {
				parsed_url.Path = "/"
			}
			vurl, _ := g.View(URL_VIEW)
			vurl.Clear()
			for k, v := range parsed_url.Query() {
				for _, vv := range v {
					fmt.Fprintf(vget, "%v=%v\n", k, vv)
				}
			}
			parsed_url.RawQuery = ""
			setViewTextAndCursor(vurl, parsed_url.String())
		}
		arg_index += 1
	}

	if set_data && !set_method {
		vmethod, _ := g.View(REQUEST_METHOD_VIEW)
		setViewTextAndCursor(vmethod, http.MethodPost)
	}

	if !set_binary_data && content_type != "" && !a.hasHeader(g, "Content-Type") {
		fmt.Fprintf(vheader, "Content-Type: %v\n", config.ContentTypes[content_type])
	}

	if len(accept_types) > 0 && !a.hasHeader(g, "Accept") {
		fmt.Fprintf(vheader, "Accept: %v\n", strings.Join(accept_types, ","))
	}

	var merged_body_data string
	if set_data && !set_binary_data {
		merged_body_data = strings.Join(body_data, "&")
	}

	vdata, _ := g.View(REQUEST_DATA_VIEW)
	setViewTextAndCursor(vdata, merged_body_data)

	return nil
}

func (a *App) hasHeader(g *gocui.Gui, h string) bool {
	for _, header := range strings.Split(getViewValue(g, REQUEST_HEADERS_VIEW), "\n") {
		if header == "" {
			continue
		}
		header_parts := strings.SplitN(header, ": ", 2)
		if len(header_parts) != 2 {
			continue
		}
		if header_parts[0] == h {
			return true
		}
	}
	return false
}

// Apply startup config values. This is run after a.ParseArgs, so that
// args can override the provided config values
func (a *App) InitConfig() {
	CLIENT.Timeout = a.config.General.Timeout.Duration
	TRANSPORT.TLSClientConfig = &tls.Config{
		InsecureSkipVerify: a.config.General.Insecure,
		MinVersion:         a.config.General.TLSVersionMin,
		MaxVersion:         a.config.General.TLSVersionMax,
	}
	CLIENT.CheckRedirect = func(_ *http.Request, _ []*http.Request) error {
		if a.config.General.FollowRedirects {
			return nil
		}
		return http.ErrUseLastResponse
	}
}

func refreshStatusLine(a *App, g *gocui.Gui) {
	sv, _ := g.View(STATUSLINE_VIEW)
	sv.BgColor = gocui.ColorDefault | gocui.AttrReverse
	sv.FgColor = gocui.ColorDefault | gocui.AttrReverse
	a.statusLine.Update(sv, a)
}

func initApp(a *App, g *gocui.Gui) {
	g.Cursor = true
	g.InputEsc = false
	g.BgColor = gocui.ColorDefault
	g.FgColor = gocui.ColorDefault
	g.SetManagerFunc(a.Layout)
}

func getViewValue(g *gocui.Gui, name string) string {
	v, err := g.View(name)
	if err != nil {
		return ""
	}
	return strings.TrimSpace(v.Buffer())
}

func setViewDefaults(v *gocui.View) {
	v.Frame = true
	v.Wrap = false
}

func setViewTextAndCursor(v *gocui.View, s string) {
	v.Clear()
	fmt.Fprint(v, s)
	v.SetCursor(len(s), 0)
}

func help() {
	fmt.Println(`wuzz - Interactive cli tool for HTTP inspection

Usage: wuzz [-H|--header HEADER]... [-d|--data|--data-binary DATA] [-X|--request METHOD] [-t|--timeout MSECS] [URL]

Other command line options:
  -c, --config PATH        Specify custom configuration file
  -e, --editor EDITOR      Specify external editor command
  -f, --file REQUEST       Load a previous request
  -F, --form DATA          Add multipart form request data and set related request headers
                           If the value starts with @ it will be handled as a file path for upload
  -h, --help               Show this
  -j, --json JSON          Add JSON request data and set related request headers
  -k, --insecure           Allow insecure SSL certs
  -R, --disable-redirects  Do not follow HTTP redirects
  -T, --tls MIN,MAX        Restrict allowed TLS versions (values: SSL3.0,TLS1.0,TLS1.1,TLS1.2)
                           Examples: wuzz -T TLS1.1        (TLS1.1 only)
                                     wuzz -T TLS1.0,TLS1.1 (from TLS1.0 up to TLS1.1)
  --tlsv1.0                Forces TLS1.0 only
  --tlsv1.1                Forces TLS1.1 only
  --tlsv1.2                Forces TLS1.2 only
  -1, --tlsv1              Forces TLS version 1.x (1.0, 1.1 or 1.2)
  -v, --version            Display version number
  -x, --proxy URL          Set HTTP(S) or SOCKS5 proxy

Key bindings:
  ctrl+r              Send request
  ctrl+s              Save response
  ctrl+e              Save request
  ctrl+f              Load request
  tab, ctrl+j         Next window
  shift+tab, ctrl+k   Previous window
  alt+h               Show history
  pageUp              Scroll up the current window
  pageDown            Scroll down the current window`,
	)
}

func main() {
	configPath := ""
	args := os.Args
	for i, arg := range os.Args {
		switch arg {
		case "-h", "--help":
			help()
			return
		case "-v", "--version":
			fmt.Printf("wuzz %v\n", VERSION)
			return
		case "-c", "--config":
			configPath = os.Args[i+1]
			args = append(os.Args[:i], os.Args[i+2:]...)
			if _, err := os.Stat(configPath); os.IsNotExist(err) {
				log.Fatal("Config file specified but does not exist: \"" + configPath + "\"")
			}
		}
	}
	var g *gocui.Gui
	var err error
	for _, outputMode := range []gocui.OutputMode{gocui.Output256, gocui.OutputNormal, gocui.OutputMode(termbox.OutputGrayscale)} {
		g, err = gocui.NewGui(outputMode)
		if err == nil {
			break
		}
	}
	if err != nil {
		log.Panicln(err)
	}

	if runtime.GOOS == WINDOWS_OS && runewidth.IsEastAsian() {
		g.ASCII = true
	}

	app := &App{history: make([]*Request, 0, 31)}

	// overwrite default editor
	defaultEditor = ViewEditor{app, g, false, gocui.DefaultEditor}

	initApp(app, g)

	// load config (must be done *before* app.ParseArgs, as arguments
	// should be able to override config values). An empty string passed
	// to LoadConfig results in LoadConfig loading the default config
	// location. If there is no config, the values in
	// config.DefaultConfig will be used.
	err = app.LoadConfig(configPath)
	if err != nil {
		g.Close()
		log.Fatalf("Error loading config file: %v", err)
	}

	err = app.ParseArgs(g, args)

	// Some of the values in the config need to have some startup
	// behavior associated with them. This is run after ParseArgs so
	// that command-line arguments can override configuration values.
	app.InitConfig()

	if err != nil {
		g.Close()
		fmt.Println("Error!", err)
		os.Exit(1)
	}

	err = app.SetKeys(g)

	if err != nil {
		g.Close()
		fmt.Println("Error!", err)
		os.Exit(1)
	}

	defer g.Close()

	if err := g.MainLoop(); err != nil && err != gocui.ErrQuit {
		log.Panicln(err)
	}
}

func exportJSON(r Request) []byte {
	requestMap := map[string]string{
		URL_VIEW:             r.Url,
		REQUEST_METHOD_VIEW:  r.Method,
		URL_PARAMS_VIEW:      r.GetParams,
		REQUEST_DATA_VIEW:    r.Data,
		REQUEST_HEADERS_VIEW: r.Headers,
	}

	request, err := json.Marshal(requestMap)
	if err != nil {
		return []byte{}
	}
	return request
}

func exportCurl(r Request) []byte {
	var headers, params string
	for _, header := range strings.Split(r.Headers, "\n") {
		if header == "" {
			continue
		}
		headers = fmt.Sprintf("%s -H %s", headers, shellescape.Quote(header))
	}
	if r.GetParams != "" {
		params = fmt.Sprintf("?%s", r.GetParams)
	}
	return []byte(fmt.Sprintf("curl %s -X %s -d %s %s\n", headers, r.Method, shellescape.Quote(r.Data), shellescape.Quote(r.Url+params)))
}
