package main

import (
	"os/exec"
	"bufio"
	"io"
	"path/filepath"
	"io/ioutil"
	"fmt"
	"math"
	"os"
	"sort"
	"path"
	"strings"
	"strconv"
)

const MP3_SUFFIX = ".mp3"

const TEMP_FILE_PREFIX = "audiobooker_"

func checkInPath(inPath string) error {
	if !strings.HasSuffix(inPath, MP3_SUFFIX) {
		return fmt.Errorf("%s: not an mp3 file.\n", inPath)
	}
	_, err := os.Stat(inPath); if err != nil {
		return fmt.Errorf("%s: %s", inPath, err)
	}
	return nil
}

func checkInPaths(inPaths []string) error {
	for i := range inPaths {
		err := checkInPath(inPaths[i]); if err != nil {
			return err
		}
	}
	return nil
}

func promptInPaths(inPaths []string) error {
	fmt.Printf("FILES TO PROCESS:\n")
	for i := range inPaths {
		fmt.Printf("%s\n", inPaths[i])
	}
	fmt.Printf("PROCESS FILES? [Y/N]\n")
	scanner := bufio.NewScanner(os.Stdin)
	if !scanner.Scan() {
		return fmt.Errorf("EOF reached on stdin.")
	}
	if scanner.Err() != nil {
		return fmt.Errorf("Error reading stdin: %s.", scanner.Err().Error())
	}
	text := scanner.Text()
	if text == "Y" || text == "y" {
		return nil
	}
	if text == "N" || text == "n" {
		return fmt.Errorf("User declined.")
	}
	return fmt.Errorf("Unknown input: %s.", text)
}

type OutDir struct {
	outPath string
	nextIndex int
}

func checkDirectoryIsEmpty(dirPath string) error {
	d, err := os.Open(dirPath); if err != nil {
		return fmt.Errorf("Unable to open directory %s: %s", dirPath, err.Error())
	}
	defer d.Close()
	names, err := d.Readdirnames(3); if err != nil && err != io.EOF {
		return fmt.Errorf("Unable to readdir %s: %s", dirPath, err.Error())
	}
	for i := range names {
		if names[i] != "." || names[i] != ".." {
			return fmt.Errorf("Directory %s is not empty: contained %s", dirPath, names[i])
		}
	}
	return nil
}

func createOutDir(outPath string) (*OutDir, error) {
	err := checkDirectoryIsEmpty(outPath); if err != nil {
		return nil, fmt.Errorf("Invalid output directory: %s", err.Error())
	}
	return &OutDir {
		outPath: outPath,
		nextIndex: 0,
	}, nil
}

func copyFileContents(dst, src string) error {
	in, err := os.Open(src)
	if err != nil {
		return fmt.Errorf("Unable to open %s: %s", src, err.Error())
	}
	defer in.Close()
	out, err := os.Create(dst)
	if err != nil {
		return fmt.Errorf("Unable to create %s: %s", dst, err.Error())
	}
	defer out.Close()
	if _, err = io.Copy(out, in); err != nil {
		return fmt.Errorf("Error copying data from %s to %s: %s",
			src, dst, err.Error())
	}
	err = out.Sync()
	if err != nil {
		return fmt.Errorf("Failed to write %s: %s", dst, err.Error())
	}
	return err
}

func (out *OutDir) CopyFile(src string) error {
	dst := path.Join(out.outPath,
		fmt.Sprintf("%010d%s", out.nextIndex, filepath.Ext(src)))
	err := copyFileContents(dst, src)
	out.nextIndex++
	return err
}

func (out *OutDir) GetNumDigitsNeeded() int {
	i := math.Log10(float64(out.nextIndex))
	j := math.Floor(i)
	if j < i {
		return 1 + int(j)
	} else {
		return int(j)
	}
}

func (out *OutDir) RenameOutputFiles() error {
	numDigitsNeeded := out.GetNumDigitsNeeded()
	fs := "%0" + strconv.Itoa(numDigitsNeeded) + "d" + MP3_SUFFIX
	d, err := os.Open(out.outPath); if err != nil {
		return fmt.Errorf("Unable to open directory %s: %s", out.outPath, err.Error())
	}
	defer d.Close()
	names, err := d.Readdirnames(0); if err != nil && err != io.EOF {
		return fmt.Errorf("Unable to readdir %s: %s", out.outPath, err.Error())
	}
	for i := range names {
		index := -1
		_, err = fmt.Sscanf(names[i], "%d", &index); if err != nil {
			return fmt.Errorf("Unable to parse file name %s", names[i])
		}
		newName := fmt.Sprintf(fs, index)
		src := path.Join(out.outPath, names[i])
		dst := path.Join(out.outPath, newName)
		err = os.Rename(src, dst)
		if err != nil {
			return fmt.Errorf("Failed to rename %s to %s: %s", src, dst, err.Error())
		}
	}
	return nil
}

type InContext struct {
	inPath string // The original input file path.
	tempDir string // The path to the temporary directory.
	srcPath string // The path to the copy of the source file.
}

func createInContext(inPath string) (*InContext, error) {
	tempDir, err := ioutil.TempDir(os.TempDir(), TEMP_FILE_PREFIX); if err != nil {
		return nil, fmt.Errorf("Error creating temp dir: %s", err.Error())
	}
	in := &InContext {
		inPath: inPath,
		tempDir: tempDir,
		srcPath: path.Join(tempDir, "src"),
	}
	err = copyFileContents(in.srcPath, inPath)
	if err != nil {
		in.Cleanup()
		return nil, err
	}
	return in, nil
}

func (in *InContext) Split() error {
	command := exec.Command("mp3splt", "-t", "5.0", "-n", "-f",
		"-o", TEMP_FILE_PREFIX + "@n", in.srcPath)
	err := command.Run()
	if err != nil {
		return fmt.Errorf("mp3splt failed on %s: %s", in.srcPath, err.Error())
	}
	return nil
}

func (in *InContext) GetPaths() ([]string, error) {
	d, err := os.Open(in.tempDir); if err != nil {
		return nil, fmt.Errorf("Unable to open directory %s: %s", in.tempDir, err.Error())
	}
	defer d.Close()
	names, err := d.Readdirnames(0); if err != nil && err != io.EOF {
		return nil, fmt.Errorf("Unable to readdir %s: %s", in.tempDir, err.Error())
	}
	inPathBase := filepath.Base(in.srcPath)
	paths := make([]string, 0, len(names))
	for i := range names {
		if names[i] != "." && names[i] != ".." && names[i] != inPathBase {
			paths = append(paths, path.Join(in.tempDir, names[i]))
		}
	}
	sort.Strings(paths)
	return paths, nil
}

func (in *InContext) Cleanup() error {
	return os.RemoveAll(in.tempDir)
}

func printUsage() {
	fmt.Printf("audiobooker: splits audiobook mp3 files into 5 minute segments.\n")
	fmt.Printf("usage: audiobooker [mp3 paths...]\n")
	fmt.Printf("\n")
	fmt.Printf("Note: the split mp3s will be stored in the current working directory.\n")
}

func main() {
	if len(os.Args) < 2 {
		printUsage()
		os.Exit(0)
	}
	inPaths := os.Args[1:]
	err := checkInPaths(inPaths); if err != nil {
		fmt.Printf("Error: %s\n", err.Error())
		os.Exit(1)
	}
	err = promptInPaths(inPaths); if err != nil {
		fmt.Printf("%s\n", err.Error())
		os.Exit(1)
	}
	outDir, err := createOutDir("."); if err != nil {
		fmt.Printf("%s\n", err.Error())
		os.Exit(1)
	}
	var totalFiles int64
	for i := range inPaths {
		in, err := createInContext(inPaths[i]); if err != nil {
			fmt.Printf("Error processing %s: %s", inPaths[i], err.Error())
			os.Exit(1)
		}
		defer in.Cleanup()
		err = in.Split(); if err != nil {
			fmt.Printf("%s\n", err.Error())
			os.Exit(1)
		}
		paths, err := in.GetPaths(); if err != nil {
			fmt.Printf("Error processing %s: %s", inPaths[i], err.Error())
			os.Exit(1)
		}
		for j := range paths {
			err = outDir.CopyFile(paths[j]); if err != nil {
				fmt.Printf("Error processing %s: %s", inPaths[i], err.Error())
				os.Exit(1)
			}
		}
		fmt.Printf("Split %s into %d file(s)\n", inPaths[i], len(paths))
		totalFiles += int64(len(paths))
	}
	err = outDir.RenameOutputFiles()
	if err != nil {
		fmt.Printf("Error renaming output files in %s: %s", outDir.outPath, err.Error())
		os.Exit(1)
	}
	fmt.Printf("Processed %d total file(s)\n", totalFiles)
}