// Copyright (c) 2005, Rodrigo Braz Monteiro
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
//   * Redistributions of source code must retain the above copyright notice,
//     this list of conditions and the following disclaimer.
//   * Redistributions in binary form must reproduce the above copyright notice,
//     this list of conditions and the following disclaimer in the documentation
//     and/or other materials provided with the distribution.
//   * Neither the name of the Aegisub Group nor the names of its contributors
//     may be used to endorse or promote products derived from this software
//     without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
//
// Aegisub Project http://www.aegisub.org/
//
// $Id$

/// @file video_slider.cpp
/// @brief Seek-bar control for video
/// @ingroup custom_control
///


////////////
// Includes
#include "config.h"

#include <wx/wxprec.h>
#include <wx/settings.h>
#include "video_slider.h"
#include "video_display.h"
#include "subs_grid.h"
#include "ass_dialogue.h"
#include "vfr.h"
#include "subs_edit_box.h"
#include "options.h"
#include "utils.h"



/// @brief Constructor 
/// @param parent 
/// @param id     
///
VideoSlider::VideoSlider (wxWindow* parent, wxWindowID id)
: wxWindow (parent,id,wxDefaultPosition,wxDefaultSize,wxWANTS_CHARS | wxFULL_REPAINT_ON_RESIZE)
{
	val = 0;
	min = 0;
	max = 1;
	Display = NULL;
	SetClientSize(20,25);
	locked = false;
	SetRange(0,1);
}



/// @brief Get value 
/// @return 
///
int VideoSlider::GetValue() {
	return val;
}



/// @brief Set value 
/// @param value 
/// @return 
///
void VideoSlider::SetValue(int value) {
	if (locked) return;
	val = value;
	if (val < min) val = min;
	if (val > max) val = max;
	Refresh(false);
}



/// @brief Set range 
/// @param from 
/// @param to   
///
void VideoSlider::SetRange(int from,int to) {
	if (from > to) from = to;
	locked = false;
	min = from;
	max = to;
	if (val < from) val = from;
	if (val > to) val = to;
}



/// @brief Get value at X 
/// @param x 
/// @return 
///
int VideoSlider::GetValueAtX(int x) {
	// Get dimensions
	int w,h;
	GetClientSize(&w,&h);

	// Special case
	if (w <= 10) return 0;

	// Calculate
	return (int64_t)(x-5)*(int64_t)(max-min)/(int64_t)(w-10)+min;
}



/// @brief Get X at value 
/// @param value 
/// @return 
///
int VideoSlider::GetXAtValue(int value) {
	// Get dimensions
	int w,h;
	GetClientSize(&w,&h);

	// Special case
	if (max-min <= 0) return 0;

	// Calculate
	return (int64_t)(value-min)*(int64_t)(w-10)/(int64_t)(max-min)+5;
}



/// @brief Next frame hotkey 
/// @return 
///
void VideoSlider::NextFrame() {
	if (VideoContext::Get()->IsPlaying()) return;

	//don't request out of range frames
	if (GetValue() < max) VideoContext::Get()->JumpToFrame(GetValue()+1);
	Refresh(false);
	Update();
}



/// @brief Previous frame hotkey 
/// @return 
///
void VideoSlider::PrevFrame() {
	if (VideoContext::Get()->IsPlaying()) return;

	//don't request out of range frames
	if (GetValue() > min) VideoContext::Get()->JumpToFrame(GetValue()-1);
	Refresh(false);
	Update();
}


///////////////
// Event table
BEGIN_EVENT_TABLE(VideoSlider, wxWindow)
	EVT_MOUSE_EVENTS(VideoSlider::OnMouse)
	EVT_KEY_DOWN(VideoSlider::OnKeyDown)
	EVT_PAINT(VideoSlider::OnPaint)
	EVT_SET_FOCUS(VideoSlider::OnFocus)
	EVT_KILL_FOCUS(VideoSlider::OnFocus)
	EVT_ERASE_BACKGROUND(VideoSlider::OnEraseBackground)
END_EVENT_TABLE()



/// @brief Change position 
/// @return 
///
void VideoSlider::UpdateVideo() {
	if (Display) {
		if (VideoContext::Get()->IsPlaying()) return;
		locked = true;
		VideoContext::Get()->JumpToFrame(GetValue());
		locked = false;
	}
}



/// @brief Mouse events 
/// @param event 
/// @return 
///
void VideoSlider::OnMouse(wxMouseEvent &event) {
	// Coordinates
	int x = event.GetX();
	//int y = event.GetY();
	bool shift = event.m_shiftDown;

	// Left click
	if (event.ButtonIsDown(wxMOUSE_BTN_LEFT)) {
		// Check if it's OK to drag
		bool canDrag = wxWindow::FindFocus() == this;
		if (!canDrag) {
			int tolerance = 4;
			int curX = GetXAtValue(GetValue());
			if (x-curX < -tolerance || x-curX > tolerance) canDrag = true;
		}

		// Drag
		if (canDrag) {
			// Shift click to snap to keyframe
			if (shift && Display) {
				wxArrayInt KeyFrames = VideoContext::Get()->GetKeyFrames();
				int keys = KeyFrames.Count();
				int clickedFrame = GetValueAtX(x);
				int closest = 0;
				int cur;

				// Find closest
				for (int i=0;i<keys;i++) {
					cur = KeyFrames[i];
					if (abs(cur-clickedFrame) < abs(closest-clickedFrame)) {
						closest = cur;
					}
				}

				// Jump to frame
				if (closest == GetValue()) return;
				SetValue(closest);
			}

			// Normal click
			else {
				int go = GetValueAtX(x);
				if (go == GetValue()) return;
				SetValue(go);
			}
			Refresh(false);

			// Playing?
			if (VideoContext::Get()->IsPlaying()) {
				VideoContext::Get()->Stop();
				UpdateVideo();
				VideoContext::Get()->Play();
			}
			else UpdateVideo();
		}

		// Get focus
		SetFocus();
	}

	// Right/middle click
	if (event.ButtonDown(wxMOUSE_BTN_RIGHT) || event.ButtonDown(wxMOUSE_BTN_MIDDLE)) {
		SetFocus();
	}

	// Something else
	else if (!VideoContext::Get()->IsPlaying()) event.Skip();
}



/// @brief Key down event 
/// @param event 
/// @return 
///
void VideoSlider::OnKeyDown(wxKeyEvent &event) {
	if (VideoContext::Get()->IsPlaying()) return;

	// Get flags
	int key = event.GetKeyCode();
#ifdef __APPLE__
	bool ctrl = event.m_metaDown;
#else
	bool ctrl = event.m_controlDown;
#endif
	bool alt = event.m_altDown;
	bool shift = event.m_shiftDown;
	int direction = 0;

	// Pick direction
	if (key == WXK_LEFT) direction = -1;
	else if (key == WXK_RIGHT) direction = 1;

	// If a direction was actually pressed
	if (direction) {
		// Standard move
		if (!ctrl && !shift && !alt) {
			if (direction == 1) NextFrame();
			else PrevFrame();
			return;
		}

		// Fast move
		if (!ctrl && !shift && alt) {
			if (VideoContext::Get()->IsPlaying()) return;
			int target = MID(min,GetValue() + direction * Options.AsInt(_T("Video Fast Jump Step")),max);
			if (target != GetValue()) VideoContext::Get()->JumpToFrame(target);
			return;
		}

		// Boundaries
		if (ctrl && !shift && !alt) {
			// Prepare
			wxArrayInt sel = grid->GetSelection();
			int cur;
			if (sel.Count() > 0) cur = sel[0];
			else {
				grid->editBox->SetToLine(0);
				grid->SelectRow(0);
				cur = 0;
			}
			AssDialogue *curDiag = grid->GetDialogue(cur);
			if (!curDiag) return;

			// Jump to next sub boundary
			if (direction != 0) {
				int target1 = VFR_Output.GetFrameAtTime(curDiag->Start.GetMS(),true);
				int target2 = VFR_Output.GetFrameAtTime(curDiag->End.GetMS(),false);
				bool drawn = false;

				// Forward
				if (direction == 1) {
					if (VideoContext::Get()->GetFrameN() < target1) VideoContext::Get()->JumpToFrame(target1);
					else if (VideoContext::Get()->GetFrameN() < target2) VideoContext::Get()->JumpToFrame(target2);
					else {
						if (cur+1 >= grid->GetRows()) return;
						grid->editBox->SetToLine(cur+1);
						grid->SelectRow(cur+1);
						grid->MakeCellVisible(cur+1,0);
						grid->SetVideoToSubs(true);
						grid->Refresh(false);
						drawn = true;
					}
					return;
				}

				// Backward
				else {
					if (VideoContext::Get()->GetFrameN() > target2) VideoContext::Get()->JumpToFrame(target2);
					else if (VideoContext::Get()->GetFrameN() > target1) VideoContext::Get()->JumpToFrame(target1);
					else {
						if (cur-1 < 0) return;
						grid->editBox->SetToLine(cur-1);
						grid->SelectRow(cur-1);
						grid->MakeCellVisible(cur-1,0);
						grid->SetVideoToSubs(false);
						grid->Refresh(false);
						drawn = true;
					}
					return;
				}
			}
		}

		// Snap to keyframe
		if (shift && !ctrl && !alt) {
			if (direction != 0) {
				// Prepare
				int prevKey = 0;
				int nextKey = VideoContext::Get()->GetLength()-1;
				wxArrayInt KeyFrames = VideoContext::Get()->GetKeyFrames();
				int keys = KeyFrames.Count();
				int cur = VideoContext::Get()->GetFrameN();
				int i;
				int temp;

				// Find previous keyframe
				// This algorithm does unnecessary loops, but it ensures it works even if keyframes are out of order.
				for (i=0;i<keys;i++) {
					temp = KeyFrames[i];
					if (temp < cur && temp > prevKey) prevKey = temp;
				}

				// Find next keyframe
				for (i=0;i<keys;i++) {
					temp = KeyFrames[i];
					if (temp > cur && temp < nextKey) nextKey = KeyFrames[i];
				}

				if (direction == -1) VideoContext::Get()->JumpToFrame(prevKey);
				if (direction == 1) VideoContext::Get()->JumpToFrame(nextKey);
				return;
			}
		}
	}

	// Forward up/down to grid
	if (key == WXK_UP || key == WXK_DOWN) {
		grid->GetEventHandler()->ProcessEvent(event);
		grid->SetFocus();
		return;
	}

	event.Skip();
}



/// @brief Paint event 
/// @param event 
///
void VideoSlider::OnPaint(wxPaintEvent &event) {
	wxPaintDC dc(this);
	DrawImage(dc);
}



/// @brief Draw image 
/// @param destdc 
///
void VideoSlider::DrawImage(wxDC &destdc) {
	// Get dimensions
	int w,h;
	GetClientSize(&w,&h);

	// Back buffer
	wxMemoryDC dc;
	wxBitmap bmp(w,h);
	dc.SelectObject(bmp);

	// Colors
	wxColour shad = wxSystemSettings::GetColour(wxSYS_COLOUR_3DDKSHADOW);
	wxColour high = wxSystemSettings::GetColour(wxSYS_COLOUR_3DLIGHT);
	wxColour face = wxSystemSettings::GetColour(wxSYS_COLOUR_3DFACE);
	wxColour sel(123,251,232);
	wxColour notSel(sel.Red()*2/5,sel.Green()*2/5,sel.Blue()*2/5);
	wxColour bord(0,0,0);
	int x1,x2,y1,y2;

	// Background
	dc.SetPen(*wxTRANSPARENT_PEN);
	dc.SetBrush(face);
	dc.DrawRectangle(0,0,w,h);

	// Selection border
	bool selected = wxWindow::FindFocus() == this;
	if (selected) {
		dc.SetBrush(*wxTRANSPARENT_BRUSH);
		dc.SetPen(wxPen(shad,1,wxDOT));
		dc.DrawRectangle(0,0,w,h);
	}

	// Draw slider
	x1 = 5;
	x2 = w-5;
	y1 = 8;
	y2 = h-8;
	dc.SetPen(wxPen(shad));
	dc.DrawLine(x1,y1,x2,y1);
	dc.DrawLine(x1,y1,x1,y2);
	dc.SetPen(wxPen(high));
	dc.DrawLine(x1,y2,x2,y2);
	dc.DrawLine(x2,y1,x2,y2);

	// Draw keyframes
	int curX;
	if (Display && Options.AsBool(_T("Show keyframes on video slider"))) {
		dc.SetPen(wxPen(shad));
		wxArrayInt KeyFrames = VideoContext::Get()->GetKeyFrames();
		int keys = KeyFrames.Count();
		for (int i=0;i<keys;i++) {
			curX = GetXAtValue(KeyFrames[i]);
			dc.DrawLine(curX,2,curX,8);
		}
	}

	// Draw cursor
	curX = GetXAtValue(GetValue());

	// Fill bg
	dc.SetBrush(wxBrush(face));
	dc.SetPen(*wxTRANSPARENT_PEN);
	dc.DrawRectangle(curX-2,y1-1,4,y2-y1+5);
	dc.SetBrush(wxNullBrush);

	// Draw cursor highlights
	dc.SetPen(wxPen(high));
	dc.DrawLine(curX,y1-2,curX-4,y1+2);
	dc.DrawLine(curX-3,y1+2,curX-3,y2+5);

	// Draw cursor shades
	dc.SetPen(wxPen(shad));
	dc.DrawLine(curX+1,y1-1,curX+4,y1+2);
	dc.DrawLine(curX+3,y1+2,curX+3,y2+5);
	dc.DrawLine(curX-3,y2+4,curX+3,y2+4);

	// Draw cursor outline
	dc.SetPen(wxPen(bord));
	dc.DrawLine(curX,y1-3,curX-4,y1+1);
	dc.DrawLine(curX,y1-3,curX+4,y1+1);
	dc.DrawLine(curX-4,y1+1,curX-4,y2+5);
	dc.DrawLine(curX+4,y1+1,curX+4,y2+5);
	dc.DrawLine(curX-3,y2+5,curX+4,y2+5);
	dc.DrawLine(curX-3,y2,curX+4,y2);

	// Draw selection
	dc.SetPen(*wxTRANSPARENT_PEN);
	if (selected) dc.SetBrush(wxBrush(sel));
	else dc.SetBrush(wxBrush(notSel));
	dc.DrawRectangle(curX-3,y2+1,7,4);

	// Draw final
	destdc.Blit(0,0,w,h,&dc,0,0);
}



/// @brief Update image 
///
void VideoSlider::UpdateImage () {
	//wxClientDC dc(this);
	//DrawImage(dc);
	Refresh(false);
}



/// @brief Focus change 
/// @param event 
///
void VideoSlider::OnFocus(wxFocusEvent &event) {
	Refresh(false);
}


