// Copyright 2016 Hajime Hoshi
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//go:build example
// +build example

// This is an example to implement an audio player.
// See examples/wav for a simpler example to play a sound file.

package main

import (
	"bytes"
	"fmt"
	"image"
	"image/color"
	_ "image/png"
	"io"
	"io/ioutil"
	"log"
	"time"

	"github.com/hajimehoshi/ebiten/v2"
	"github.com/hajimehoshi/ebiten/v2/audio"
	"github.com/hajimehoshi/ebiten/v2/audio/mp3"
	"github.com/hajimehoshi/ebiten/v2/audio/vorbis"
	"github.com/hajimehoshi/ebiten/v2/audio/wav"
	"github.com/hajimehoshi/ebiten/v2/ebitenutil"
	raudio "github.com/hajimehoshi/ebiten/v2/examples/resources/audio"
	riaudio "github.com/hajimehoshi/ebiten/v2/examples/resources/images/audio"
	"github.com/hajimehoshi/ebiten/v2/inpututil"
)

const (
	screenWidth  = 640
	screenHeight = 480

	sampleRate = 32000
)

var (
	playerBarColor     = color.RGBA{0x80, 0x80, 0x80, 0xff}
	playerCurrentColor = color.RGBA{0xff, 0xff, 0xff, 0xff}
)

var (
	playButtonImage  *ebiten.Image
	pauseButtonImage *ebiten.Image
	alertButtonImage *ebiten.Image
)

func init() {
	img, _, err := image.Decode(bytes.NewReader(riaudio.Play_png))
	if err != nil {
		panic(err)
	}
	playButtonImage = ebiten.NewImageFromImage(img)

	img, _, err = image.Decode(bytes.NewReader(riaudio.Pause_png))
	if err != nil {
		panic(err)
	}
	pauseButtonImage = ebiten.NewImageFromImage(img)

	img, _, err = image.Decode(bytes.NewReader(riaudio.Alert_png))
	if err != nil {
		panic(err)
	}
	alertButtonImage = ebiten.NewImageFromImage(img)
}

type musicType int

const (
	typeOgg musicType = iota
	typeMP3
)

func (t musicType) String() string {
	switch t {
	case typeOgg:
		return "Ogg"
	case typeMP3:
		return "MP3"
	default:
		panic("not reached")
	}
}

// Player represents the current audio state.
type Player struct {
	game         *Game
	audioContext *audio.Context
	audioPlayer  *audio.Player
	current      time.Duration
	total        time.Duration
	seBytes      []byte
	seCh         chan []byte
	volume128    int
	musicType    musicType

	playButtonPosition  image.Point
	alertButtonPosition image.Point
}

func playerBarRect() (x, y, w, h int) {
	w, h = 600, 8
	x = (screenWidth - w) / 2
	y = screenHeight - h - 16
	return
}

func NewPlayer(game *Game, audioContext *audio.Context, musicType musicType) (*Player, error) {
	type audioStream interface {
		io.ReadSeeker
		Length() int64
	}

	const bytesPerSample = 4 // TODO: This should be defined in audio package

	var s audioStream

	switch musicType {
	case typeOgg:
		var err error
		s, err = vorbis.DecodeWithoutResampling(bytes.NewReader(raudio.Ragtime_ogg))
		if err != nil {
			return nil, err
		}
	case typeMP3:
		var err error
		s, err = mp3.DecodeWithoutResampling(bytes.NewReader(raudio.Ragtime_mp3))
		if err != nil {
			return nil, err
		}
	default:
		panic("not reached")
	}
	p, err := audioContext.NewPlayer(s)
	if err != nil {
		return nil, err
	}
	player := &Player{
		game:         game,
		audioContext: audioContext,
		audioPlayer:  p,
		total:        time.Second * time.Duration(s.Length()) / bytesPerSample / sampleRate,
		volume128:    128,
		seCh:         make(chan []byte),
		musicType:    musicType,
	}
	if player.total == 0 {
		player.total = 1
	}

	const buttonPadding = 16
	w, _ := playButtonImage.Size()
	player.playButtonPosition.X = (screenWidth - w*2 + buttonPadding*1) / 2
	player.playButtonPosition.Y = screenHeight - 160

	player.alertButtonPosition.X = player.playButtonPosition.X + w + buttonPadding
	player.alertButtonPosition.Y = player.playButtonPosition.Y

	player.audioPlayer.Play()
	go func() {
		s, err := wav.DecodeWithSampleRate(sampleRate, bytes.NewReader(raudio.Jab_wav))
		if err != nil {
			log.Fatal(err)
			return
		}
		b, err := ioutil.ReadAll(s)
		if err != nil {
			log.Fatal(err)
			return
		}
		player.seCh <- b
	}()
	return player, nil
}

func (p *Player) Close() error {
	return p.audioPlayer.Close()
}

func (p *Player) update() error {
	select {
	case p.seBytes = <-p.seCh:
		close(p.seCh)
		p.seCh = nil
	default:
	}

	if p.audioPlayer.IsPlaying() {
		p.current = p.audioPlayer.Current()
	}
	p.seekBarIfNeeded()
	p.switchPlayStateIfNeeded()
	p.playSEIfNeeded()
	p.updateVolumeIfNeeded()

	if inpututil.IsKeyJustPressed(ebiten.KeyU) {
		b := ebiten.IsRunnableOnUnfocused()
		ebiten.SetRunnableOnUnfocused(!b)
	}
	return nil
}

func (p *Player) shouldPlaySE() bool {
	if p.seBytes == nil {
		// Bytes for the SE is not loaded yet.
		return false
	}

	if inpututil.IsKeyJustPressed(ebiten.KeyP) {
		return true
	}
	r := image.Rectangle{
		Min: p.alertButtonPosition,
		Max: p.alertButtonPosition.Add(image.Pt(alertButtonImage.Size())),
	}
	if image.Pt(ebiten.CursorPosition()).In(r) {
		if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) {
			return true
		}
	}
	for _, id := range p.game.justPressedTouchIDs {
		if image.Pt(ebiten.TouchPosition(id)).In(r) {
			return true
		}
	}
	return false
}

func (p *Player) playSEIfNeeded() {
	if !p.shouldPlaySE() {
		return
	}
	sePlayer := p.audioContext.NewPlayerFromBytes(p.seBytes)
	sePlayer.Play()
}

func (p *Player) updateVolumeIfNeeded() {
	if ebiten.IsKeyPressed(ebiten.KeyZ) {
		p.volume128--
	}
	if ebiten.IsKeyPressed(ebiten.KeyX) {
		p.volume128++
	}
	if p.volume128 < 0 {
		p.volume128 = 0
	}
	if 128 < p.volume128 {
		p.volume128 = 128
	}
	p.audioPlayer.SetVolume(float64(p.volume128) / 128)
}

func (p *Player) shouldSwitchPlayStateIfNeeded() bool {
	if inpututil.IsKeyJustPressed(ebiten.KeyS) {
		return true
	}
	r := image.Rectangle{
		Min: p.playButtonPosition,
		Max: p.playButtonPosition.Add(image.Pt(playButtonImage.Size())),
	}
	if image.Pt(ebiten.CursorPosition()).In(r) {
		if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) {
			return true
		}
	}
	for _, id := range p.game.justPressedTouchIDs {
		if image.Pt(ebiten.TouchPosition(id)).In(r) {
			return true
		}
	}
	return false
}

func (p *Player) switchPlayStateIfNeeded() {
	if !p.shouldSwitchPlayStateIfNeeded() {
		return
	}
	if p.audioPlayer.IsPlaying() {
		p.audioPlayer.Pause()
		return
	}
	p.audioPlayer.Play()
}

func (p *Player) justPressedPosition() (int, int, bool) {
	if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) {
		x, y := ebiten.CursorPosition()
		return x, y, true
	}

	if len(p.game.justPressedTouchIDs) > 0 {
		x, y := ebiten.TouchPosition(p.game.justPressedTouchIDs[0])
		return x, y, true
	}

	return 0, 0, false
}

func (p *Player) seekBarIfNeeded() {
	// Calculate the next seeking position from the current cursor position.
	x, y, ok := p.justPressedPosition()
	if !ok {
		return
	}
	bx, by, bw, bh := playerBarRect()
	const padding = 4
	if y < by-padding || by+bh+padding <= y {
		return
	}
	if x < bx || bx+bw <= x {
		return
	}
	pos := time.Duration(x-bx) * p.total / time.Duration(bw)
	p.current = pos
	p.audioPlayer.Seek(pos)
}

func (p *Player) draw(screen *ebiten.Image) {
	// Draw the bar.
	x, y, w, h := playerBarRect()
	ebitenutil.DrawRect(screen, float64(x), float64(y), float64(w), float64(h), playerBarColor)

	// Draw the cursor on the bar.
	c := p.current
	cw, ch := 8, 20
	cx := int(time.Duration(w)*c/p.total) + x - cw/2
	cy := y - (ch-h)/2
	ebitenutil.DrawRect(screen, float64(cx), float64(cy), float64(cw), float64(ch), playerCurrentColor)

	// Compose the curren time text.
	m := (c / time.Minute) % 100
	s := (c / time.Second) % 60
	currentTimeStr := fmt.Sprintf("%02d:%02d", m, s)

	// Draw buttons
	op := &ebiten.DrawImageOptions{}
	op.GeoM.Translate(float64(p.playButtonPosition.X), float64(p.playButtonPosition.Y))
	if p.audioPlayer.IsPlaying() {
		screen.DrawImage(pauseButtonImage, op)
	} else {
		screen.DrawImage(playButtonImage, op)
	}
	op.GeoM.Reset()
	op.GeoM.Translate(float64(p.alertButtonPosition.X), float64(p.alertButtonPosition.Y))
	screen.DrawImage(alertButtonImage, op)

	// Draw the debug message.
	msg := fmt.Sprintf(`TPS: %0.2f
Press S to toggle Play/Pause
Press P to play SE
Press Z or X to change volume of the music
Press U to switch the runnable-on-unfocused state
Press A to switch Ogg and MP3 (Current: %s)
Current Time: %s
Current Volume: %d/128
Type: %s`, ebiten.ActualTPS(), p.musicType,
		currentTimeStr, int(p.audioPlayer.Volume()*128), p.musicType)
	ebitenutil.DebugPrint(screen, msg)
}

type Game struct {
	musicPlayer   *Player
	musicPlayerCh chan *Player
	errCh         chan error

	justPressedTouchIDs []ebiten.TouchID
}

func NewGame() (*Game, error) {
	audioContext := audio.NewContext(sampleRate)

	g := &Game{
		musicPlayerCh: make(chan *Player),
		errCh:         make(chan error),
	}

	m, err := NewPlayer(g, audioContext, typeOgg)
	if err != nil {
		return nil, err
	}

	g.musicPlayer = m
	return g, nil
}

func (g *Game) Update() error {
	select {
	case p := <-g.musicPlayerCh:
		g.musicPlayer = p
	case err := <-g.errCh:
		return err
	default:
	}

	g.justPressedTouchIDs = inpututil.AppendJustPressedTouchIDs(g.justPressedTouchIDs[:0])

	if g.musicPlayer != nil && inpututil.IsKeyJustPressed(ebiten.KeyA) {
		var t musicType
		switch g.musicPlayer.musicType {
		case typeOgg:
			t = typeMP3
		case typeMP3:
			t = typeOgg
		default:
			panic("not reached")
		}

		g.musicPlayer.Close()
		g.musicPlayer = nil

		go func() {
			p, err := NewPlayer(g, audio.CurrentContext(), t)
			if err != nil {
				g.errCh <- err
				return
			}
			g.musicPlayerCh <- p
		}()
	}

	if g.musicPlayer != nil {
		if err := g.musicPlayer.update(); err != nil {
			return err
		}
	}
	return nil
}

func (g *Game) Draw(screen *ebiten.Image) {
	if g.musicPlayer != nil {
		g.musicPlayer.draw(screen)
	}
}

func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
	return screenWidth, screenHeight
}

func main() {
	ebiten.SetWindowSize(screenWidth, screenHeight)
	ebiten.SetWindowTitle("Audio (Ebiten Demo)")
	g, err := NewGame()
	if err != nil {
		log.Fatal(err)
	}
	if err := ebiten.RunGame(g); err != nil {
		log.Fatal(err)
	}
}