# paths
makefile := $(realpath $(lastword $(MAKEFILE_LIST)))
cmd_dir  := ./cmd/cheat
dist_dir := ./dist

# parallel jobs for build-release (can be overridden)
JOBS ?= 8

# executables
CAT    := cat
COLUMN := column
CTAGS  := ctags
DOCKER := docker
GO     := go
GREP   := grep
GZIP   := gzip --best
LINT   := revive
MAN    := man
MKDIR  := mkdir -p
PANDOC := pandoc
RM     := rm
SCC    := scc
SED    := sed
SORT   := sort
ZIP    := zip -m

docker_image := cheat-devel:latest

# build flags
export CGO_ENABLED := 0
BUILD_FLAGS  := -ldflags="-s -w" -mod vendor -trimpath
GOBIN        :=
TMPDIR       := /tmp

# release binaries
releases :=                        \
	$(dist_dir)/cheat-darwin-amd64 \
	$(dist_dir)/cheat-darwin-arm64 \
	$(dist_dir)/cheat-linux-386    \
	$(dist_dir)/cheat-linux-amd64  \
	$(dist_dir)/cheat-linux-arm5   \
	$(dist_dir)/cheat-linux-arm6   \
	$(dist_dir)/cheat-linux-arm64  \
	$(dist_dir)/cheat-linux-arm7   \
	$(dist_dir)/cheat-netbsd-amd64  \
	$(dist_dir)/cheat-openbsd-amd64  \
	$(dist_dir)/cheat-solaris-amd64  \
	$(dist_dir)/cheat-windows-amd64.exe

## build: build an executable for your architecture
.PHONY: build
build: | clean $(dist_dir) fmt lint vet vendor man
	$(GO) build $(BUILD_FLAGS) -o $(dist_dir)/cheat $(cmd_dir)

## build-release: build release executables
# Runs prepare once, then builds all binaries in parallel
# Override jobs with: make build-release JOBS=16
.PHONY: build-release
build-release: prepare
	$(MAKE) -j$(JOBS) $(releases)

# cheat-darwin-amd64
$(dist_dir)/cheat-darwin-amd64:
	GOARCH=amd64 GOOS=darwin \
	$(GO) build $(BUILD_FLAGS) -o $@ $(cmd_dir) && $(GZIP) $@ && chmod -x $@.gz

# cheat-darwin-arm64
$(dist_dir)/cheat-darwin-arm64:
	GOARCH=arm64 GOOS=darwin \
	$(GO) build $(BUILD_FLAGS) -o $@ $(cmd_dir) && $(GZIP) $@ && chmod -x $@.gz

# cheat-linux-386
$(dist_dir)/cheat-linux-386:
	GOARCH=386 GOOS=linux \
	$(GO) build $(BUILD_FLAGS) -o $@ $(cmd_dir) && $(GZIP) $@ && chmod -x $@.gz

# cheat-linux-amd64
$(dist_dir)/cheat-linux-amd64:
	GOARCH=amd64 GOOS=linux \
	$(GO) build $(BUILD_FLAGS) -o $@ $(cmd_dir) && $(GZIP) $@ && chmod -x $@.gz

# cheat-linux-arm5
$(dist_dir)/cheat-linux-arm5:
	GOARCH=arm GOOS=linux GOARM=5 \
	$(GO) build $(BUILD_FLAGS) -o $@ $(cmd_dir) && $(GZIP) $@ && chmod -x $@.gz

# cheat-linux-arm6
$(dist_dir)/cheat-linux-arm6:
	GOARCH=arm GOOS=linux GOARM=6 \
	$(GO) build $(BUILD_FLAGS) -o $@ $(cmd_dir) && $(GZIP) $@ && chmod -x $@.gz

# cheat-linux-arm7
$(dist_dir)/cheat-linux-arm7:
	GOARCH=arm GOOS=linux GOARM=7 \
	$(GO) build $(BUILD_FLAGS) -o $@ $(cmd_dir) && $(GZIP) $@ && chmod -x $@.gz
	
# cheat-linux-arm64
$(dist_dir)/cheat-linux-arm64:
	GOARCH=arm64 GOOS=linux \
	$(GO) build $(BUILD_FLAGS) -o $@ $(cmd_dir) && $(GZIP) $@ && chmod -x $@.gz

# cheat-netbsd-amd64
$(dist_dir)/cheat-netbsd-amd64:
	GOARCH=amd64 GOOS=netbsd \
	$(GO) build $(BUILD_FLAGS) -o $@ $(cmd_dir) && $(GZIP) $@ && chmod -x $@.gz

# cheat-openbsd-amd64
$(dist_dir)/cheat-openbsd-amd64:
	GOARCH=amd64 GOOS=openbsd \
	$(GO) build $(BUILD_FLAGS) -o $@ $(cmd_dir) && $(GZIP) $@ && chmod -x $@.gz

# cheat-plan9-amd64
$(dist_dir)/cheat-plan9-amd64:
	GOARCH=amd64 GOOS=plan9 \
	$(GO) build $(BUILD_FLAGS) -o $@ $(cmd_dir) && $(GZIP) $@ && chmod -x $@.gz

# cheat-solaris-amd64
$(dist_dir)/cheat-solaris-amd64:
	GOARCH=amd64 GOOS=solaris \
	$(GO) build $(BUILD_FLAGS) -o $@ $(cmd_dir) && $(GZIP) $@ && chmod -x $@.gz

# cheat-windows-amd64
$(dist_dir)/cheat-windows-amd64.exe:
	GOARCH=amd64 GOOS=windows \
	$(GO) build $(BUILD_FLAGS) -o $@ $(cmd_dir) && $(ZIP) $@.zip $@ -j

# ./dist
$(dist_dir):
	$(MKDIR) $(dist_dir)

# .tmp
.tmp:
	$(MKDIR) .tmp

## install: build and install cheat on your PATH
.PHONY: install
install: build
	$(GO) install $(BUILD_FLAGS) $(GOBIN) $(cmd_dir) 

## clean: remove compiled executables
.PHONY: clean
clean:
	$(RM) -f $(dist_dir)/*
	$(RM) -rf .tmp

## distclean: remove the tags file
.PHONY: distclean
distclean:
	$(RM) -f tags
	@$(DOCKER) image rm -f $(docker_image)

## setup: install revive (linter) and scc (sloc tool)
.PHONY: setup
setup:
	$(GO) install github.com/boyter/scc@latest
	$(GO) install github.com/mgechev/revive@latest

## sloc: count "semantic lines of code"
.PHONY: sloc
sloc:
	$(SCC) --exclude-dir=vendor

## tags: build a tags file
.PHONY: tags
tags:
	$(CTAGS) -R --exclude=vendor --languages=go

## man: build a man page
# NB: pandoc may not be installed, so we're ignoring this error on failure
.PHONY: man
man:
	-$(PANDOC) -s -t man doc/cheat.1.md -o doc/cheat.1

## vendor: download, tidy, and verify dependencies
.PHONY: vendor
vendor:
	$(GO) mod vendor && $(GO) mod tidy && $(GO) mod verify

## vendor-update: update vendored dependencies
.PHONY: vendor-update
vendor-update:
	$(GO) get -t -u ./... && $(GO) mod vendor && $(GO) mod tidy && $(GO) mod verify

## fmt: run go fmt
.PHONY: fmt
fmt:
	$(GO) fmt ./...

## lint: lint go source files
.PHONY: lint
lint: vendor
	$(LINT) -exclude vendor/... ./...

## vet: vet go source files
.PHONY: vet
vet:
	$(GO) vet ./...

## test: run unit-tests
.PHONY: test
test: 
	$(GO) test ./...

## test-integration: run integration tests (requires network)
.PHONY: test-integration
test-integration:
	$(GO) test -tags=integration -count=1 ./...

## test-all: run all tests (unit and integration)
.PHONY: test-all
test-all: test test-integration

## test-fuzz: run quick fuzz tests for security-critical functions
.PHONY: test-fuzz
test-fuzz:
	@./build/fuzz.sh 15s

## test-fuzz-long: run extended fuzz tests (10 minutes each)
.PHONY: test-fuzz-long
test-fuzz-long:
	@./build/fuzz.sh 10m

## coverage: generate a test coverage report
.PHONY: coverage
coverage: .tmp
	$(GO) test ./... -coverprofile=.tmp/cheat-coverage.out && \
	$(GO) tool cover -html=.tmp/cheat-coverage.out -o .tmp/cheat-coverage.html && \
	echo "Coverage report generated: .tmp/cheat-coverage.html" && \
	(sensible-browser .tmp/cheat-coverage.html 2>/dev/null || \
	 xdg-open .tmp/cheat-coverage.html 2>/dev/null || \
	 open .tmp/cheat-coverage.html 2>/dev/null || \
	 echo "Please open .tmp/cheat-coverage.html in your browser")

## coverage-text: show test coverage by function in terminal
.PHONY: coverage-text
coverage-text: .tmp
	$(GO) test ./... -coverprofile=.tmp/cheat-coverage.out && \
	$(GO) tool cover -func=.tmp/cheat-coverage.out | $(SORT) -k3 -n

## benchmark: run performance benchmarks
.PHONY: benchmark
benchmark: .tmp
	$(GO) test -tags=integration -bench=. -benchtime=10s -benchmem ./cmd/cheat | tee .tmp/benchmark-latest.txt && \
	$(RM) -f cheat.test

## benchmark-cpu: run benchmarks with CPU profiling
.PHONY: benchmark-cpu
benchmark-cpu: .tmp
	$(GO) test -tags=integration -bench=. -benchtime=10s -cpuprofile=.tmp/cpu.prof ./cmd/cheat && \
	$(RM) -f cheat.test && \
	echo "CPU profile saved to .tmp/cpu.prof" && \
	echo "View with: go tool pprof -http=:8080 .tmp/cpu.prof"

## benchmark-mem: run benchmarks with memory profiling
.PHONY: benchmark-mem
benchmark-mem: .tmp
	$(GO) test -tags=integration -bench=. -benchtime=10s -benchmem -memprofile=.tmp/mem.prof ./cmd/cheat && \
	$(RM) -f cheat.test && \
	echo "Memory profile saved to .tmp/mem.prof" && \
	echo "View with: go tool pprof -http=:8080 .tmp/mem.prof"

## check: format, lint, vet, vendor, and run unit-tests
.PHONY: check
check: | vendor fmt lint vet test

.PHONY: prepare
prepare: | clean $(dist_dir) vendor fmt lint vet test

## docker-setup: create a docker image for use during development
.PHONY: docker-setup
docker-setup:
	$(DOCKER) build  -t $(docker_image) -f Dockerfile .

## docker-sh: shell into the docker development container
.PHONY: docker-sh
docker-sh:
	$(DOCKER) run -v $(shell pwd):/app -ti $(docker_image) /bin/ash

## help: display this help text
.PHONY: help
help:
	@$(CAT) $(makefile) | \
	$(SORT)             | \
	$(GREP) "^##"       | \
	$(SED) 's/## //g'   | \
	$(COLUMN) -t -s ':'
