/*
  This file is part of Subsonic.
	Subsonic is free software: you can redistribute it and/or modify
	it under the terms of the GNU General Public License as published by
	the Free Software Foundation, either version 3 of the License, or
	(at your option) any later version.
	Subsonic is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
	GNU General Public License for more details.
	You should have received a copy of the GNU General Public License
	along with Subsonic. If not, see <http://www.gnu.org/licenses/>.
	Copyright 2014 (C) Scott Jackson
*/

package github.popeen.dsub.service;

import android.os.Looper;
import android.util.Log;

import org.fourthline.cling.controlpoint.ActionCallback;
import org.fourthline.cling.controlpoint.ControlPoint;
import org.fourthline.cling.controlpoint.SubscriptionCallback;
import org.fourthline.cling.model.action.ActionInvocation;
import org.fourthline.cling.model.gena.CancelReason;
import org.fourthline.cling.model.gena.GENASubscription;
import org.fourthline.cling.model.message.UpnpResponse;
import org.fourthline.cling.model.meta.Action;
import org.fourthline.cling.model.meta.Service;
import org.fourthline.cling.model.meta.StateVariable;
import org.fourthline.cling.model.state.StateVariableValue;
import org.fourthline.cling.model.types.ServiceType;
import org.fourthline.cling.model.types.UnsignedIntegerFourBytes;
import org.fourthline.cling.support.avtransport.callback.GetPositionInfo;
import org.fourthline.cling.support.avtransport.callback.Pause;
import org.fourthline.cling.support.avtransport.callback.Play;
import org.fourthline.cling.support.avtransport.callback.Seek;
import org.fourthline.cling.support.avtransport.callback.SetAVTransportURI;
import org.fourthline.cling.support.avtransport.callback.Stop;
import org.fourthline.cling.support.avtransport.lastchange.AVTransportLastChangeParser;
import org.fourthline.cling.support.avtransport.lastchange.AVTransportVariable;
import org.fourthline.cling.support.contentdirectory.DIDLParser;
import org.fourthline.cling.support.lastchange.LastChange;
import org.fourthline.cling.support.model.DIDLContent;
import org.fourthline.cling.support.model.DIDLObject;
import org.fourthline.cling.support.model.PositionInfo;
import org.fourthline.cling.support.model.Res;
import org.fourthline.cling.support.model.SeekMode;
import org.fourthline.cling.support.model.item.Item;
import org.fourthline.cling.support.model.item.MusicTrack;
import org.fourthline.cling.support.model.item.VideoItem;
import org.fourthline.cling.support.renderingcontrol.callback.SetVolume;
import org.seamless.util.MimeType;

import java.io.File;
import java.net.URI;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.TimeZone;
import java.util.concurrent.atomic.AtomicLong;

import github.daneren2005.serverproxy.WebProxy;
import github.popeen.dsub.R;
import github.popeen.dsub.domain.DLNADevice;
import github.popeen.dsub.domain.MusicDirectory;
import github.popeen.dsub.domain.PlayerState;
import github.popeen.dsub.util.FileUtil;
import github.popeen.dsub.util.Pair;
import github.popeen.dsub.util.Util;

public class DLNAController extends RemoteController {
	private static final String TAG = DLNAController.class.getSimpleName();
	private static final long SEARCH_UPDATE_INTERVAL_SECONDS = 10L * 60L * 1000L;
	private static final long STATUS_UPDATE_INTERVAL_SECONDS = 3000L;

	DLNADevice device;
	ControlPoint controlPoint;
	SubscriptionCallback callback;
	boolean supportsSeek = false;
	boolean supportsSetupNext = false;
	boolean error = false;

	final AtomicLong lastUpdate = new AtomicLong();
	int currentPosition = 0;
	String currentPlayingURI;
	String nextPlayingURI;
	DownloadFile nextPlaying;
	boolean running = true;
	boolean hasDuration = false;
	Runnable searchDLNA = new Runnable() {
		@Override
		public void run() {
			if(controlPoint == null || !running) {
				return;
			}

			controlPoint.search();
			downloadService.postDelayed(searchDLNA, SEARCH_UPDATE_INTERVAL_SECONDS);
		}
	};

	public DLNAController(DownloadService downloadService, ControlPoint controlPoint, DLNADevice device) {
		super(downloadService);
		this.controlPoint = controlPoint;
		this.device = device;
		nextSupported = true;
	}

	@Override
	public void create(final boolean playing, final int seconds) {
		downloadService.setPlayerState(PlayerState.PREPARING);

		callback = new SubscriptionCallback(getTransportService(), 600) {
			@Override
			protected void failed(GENASubscription genaSubscription, UpnpResponse upnpResponse, Exception e, String msg) {
				Log.w(TAG, "Register subscription callback failed: " + msg, e);
			}

			@Override
			protected void established(GENASubscription genaSubscription) {
				Action seekAction = genaSubscription.getService().getAction("Seek");
				if(seekAction != null) {
					StateVariable seekMode = genaSubscription.getService().getStateVariable("A_ARG_TYPE_SeekMode");
					for(String allowedValue: seekMode.getTypeDetails().getAllowedValues()) {
						if("REL_TIME".equals(allowedValue)) {
							supportsSeek = true;
						}
					}
				}
				Action setupNextAction = genaSubscription.getService().getAction("SetNextAVTransportURI");
				if(setupNextAction != null) {
					supportsSetupNext = true;
				}

				startSong(downloadService.getCurrentPlaying(), playing, seconds);
				downloadService.postDelayed(searchDLNA, SEARCH_UPDATE_INTERVAL_SECONDS);
			}

			@Override
			protected void ended(GENASubscription genaSubscription, CancelReason cancelReason, UpnpResponse upnpResponse) {
				Log.i(TAG, "Ended subscription");
				if(cancelReason != null) {
					Log.i(TAG, "Cancel Reason: " + cancelReason.toString());
				}
				if(upnpResponse != null) {
					Log.i(TAG, "Reponse Message: " + upnpResponse.getStatusMessage());
					Log.i(TAG, "Response Details: " + upnpResponse.getResponseDetails());
				}
			}

			@Override
			protected void eventReceived(GENASubscription genaSubscription) {
				Map<String, StateVariableValue> m = genaSubscription.getCurrentValues();
				try {
					String lastChangeText = m.get("LastChange").toString();
					lastChangeText = lastChangeText.replace(",X_DLNA_SeekTime","").replace(",X_DLNA_SeekByte", "");
					LastChange lastChange = new LastChange(new AVTransportLastChangeParser(), lastChangeText);

					if (lastChange.getEventedValue(0, AVTransportVariable.TransportState.class) == null) {
						return;
					}

					switch (lastChange.getEventedValue(0, AVTransportVariable.TransportState.class).getValue()) {
						case PLAYING:
							downloadService.setPlayerState(PlayerState.STARTED);
							break;
						case PAUSED_PLAYBACK:
							downloadService.setPlayerState(PlayerState.PAUSED);
							break;
						case STOPPED:
							boolean failed = false;
							for(StateVariableValue val: m.values()) {
								if(val.toString().indexOf("TransportStatus val=\"ERROR_OCCURRED\"") != -1) {
									Log.w(TAG, "Failed to load with event: " + val.toString());
									failed = true;
								}
							}

							if(failed) {
								failedLoad();
							} else if(downloadService.getPlayerState() == PlayerState.STARTED) {
								// Played until the end
								downloadService.onSongCompleted();
							} else {
								downloadService.setPlayerState(PlayerState.STOPPED);
							}
							break;
						case TRANSITIONING:
							downloadService.setPlayerState(PlayerState.PREPARING);
							break;
						case NO_MEDIA_PRESENT:
							downloadService.setPlayerState(PlayerState.IDLE);
							break;
						default:
					}
				}
				catch (Exception e) {
					Log.w(TAG, "Failed to parse UPNP event", e);
				}
			}

			@Override
			protected void eventsMissed(GENASubscription genaSubscription, int i) {
				Log.w(TAG, "Event missed: " + i);
			}
		};
		controlPoint.execute(callback);
	}

	@Override
	public void start() {
		if(error) {
			Log.w(TAG, "Attempting to restart song");
			startSong(downloadService.getCurrentPlaying(), true, 0);
			return;
		}

		try {
			controlPoint.execute(new Play(getTransportService()) {
				@Override
				public void success(ActionInvocation invocation) {
					lastUpdate.set(System.currentTimeMillis());
					downloadService.setPlayerState(PlayerState.STARTED);
				}

				@Override
				public void failure(ActionInvocation actionInvocation, UpnpResponse upnpResponse, String msg) {
					Log.w(TAG, "Failed to start playing: " + msg);
					failedLoad();
				}
			});
		} catch(Exception e) {
			Log.w(TAG, "Failed to start", e);
		}
	}

	@Override
	public void stop() {
		try {
			controlPoint.execute(new Pause(getTransportService()) {
				@Override
				public void success(ActionInvocation invocation) {
					int secondsSinceLastUpdate = (int) ((System.currentTimeMillis() - lastUpdate.get()) / 1000L);
					currentPosition += secondsSinceLastUpdate;

					downloadService.setPlayerState(PlayerState.PAUSED);
				}

				@Override
				public void failure(ActionInvocation actionInvocation, UpnpResponse upnpResponse, String msg) {
					Log.w(TAG, "Failed to pause playing: " + msg);
				}
			});
		} catch(Exception e) {
			Log.w(TAG, "Failed to stop", e);
		}
	}

	@Override
	public void shutdown() {
		try {
			controlPoint.execute(new Stop(getTransportService()) {
				@Override
				public void failure(ActionInvocation invocation, org.fourthline.cling.model.message.UpnpResponse operation, String defaultMessage) {
					Log.w(TAG, "Stop failed: " + defaultMessage);
				}
			});
		} catch(Exception e) {
			Log.w(TAG, "Failed to shutdown", e);
		}

		if(callback != null) {
			callback.end();
			callback = null;
		}

		if(proxy != null) {
			proxy.stop();
			proxy = null;
		}

		running = false;
	}

	@Override
	public void updatePlaylist() {
		if(downloadService.getCurrentPlaying() == null) {
			startSong(null, false, 0);
		}
	}

	@Override
	public void changePosition(int seconds) {
		SimpleDateFormat df = new SimpleDateFormat("HH:mm:ss");
		df.setTimeZone(TimeZone.getTimeZone("UTC"));
		controlPoint.execute(new Seek(getTransportService(), SeekMode.REL_TIME, df.format(new Date(seconds * 1000))) {
			@SuppressWarnings("rawtypes")
			@Override
			public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMessage) {
				Log.w(TAG, "Seek failed: " + defaultMessage);
			}
		});
	}

	@Override
	public void changeTrack(int index, DownloadFile song) {
		startSong(song, true, 0);
	}

	@Override
	public void changeNextTrack(DownloadFile song) {
		setupNextSong(song);
	}

	@Override
	public void setVolume(int volume) {
		if(volume < 0) {
			volume = 0;
		} else if(volume > device.volumeMax) {
			volume = device.volumeMax;
		}

		device.volume = volume;
		try {
			controlPoint.execute(new SetVolume(device.renderer.findService(new ServiceType("schemas-upnp-org", "RenderingControl")), volume) {
				@SuppressWarnings("rawtypes")
				@Override
				public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMessage) {
					Log.w(TAG, "Set volume failed: " + defaultMessage);
				}
			});
		} catch(Exception e) {
			Log.w(TAG, "Failed to set volume");
		}
	}

	@Override
	public void updateVolume(boolean up) {
		int increment = device.volumeMax / 10;
		setVolume(device.volume + (up ? increment : -increment));
	}

	@Override
	public double getVolume() {
		return device.volume;
	}

	@Override
	public int getRemotePosition() {
		if(downloadService.getPlayerState() == PlayerState.STARTED) {
			int secondsSinceLastUpdate = (int) ((System.currentTimeMillis() - lastUpdate.get()) / 1000L);
			return currentPosition + secondsSinceLastUpdate;
		} else {
			return currentPosition;
		}
	}

	@Override
	public boolean isSeekable() {
		return supportsSeek && hasDuration;
	}

	private void startSong(final DownloadFile currentPlaying, final boolean autoStart, final int position) {
		try {
			controlPoint.execute(new Stop(getTransportService()) {
				@Override
				public void success(ActionInvocation invocation) {
					startSongRemote(currentPlaying, autoStart, position);
				}

				@Override
				public void failure(ActionInvocation invocation, org.fourthline.cling.model.message.UpnpResponse operation, String defaultMessage) {
					Log.w(TAG, "Stop failed before startSong: " + defaultMessage);
					startSongRemote(currentPlaying, autoStart, position);
				}
			});
		} catch(Exception e) {
			Log.w(TAG, "Failed to stop before startSong", e);
			startSongRemote(currentPlaying, autoStart, position);
		}
	}
	private void startSongRemote(final DownloadFile currentPlaying, final boolean autoStart, final int position) {
		if(currentPlaying == null) {
			downloadService.setPlayerState(PlayerState.IDLE);
			return;
		}
		error = false;

		downloadService.setPlayerState(PlayerState.PREPARING);

		try {
			Pair<String, String> songInfo = getSongInfo(currentPlaying);

			currentPlayingURI = songInfo.getFirst();
			controlPoint.execute(new SetAVTransportURI(getTransportService(), songInfo.getFirst(), songInfo.getSecond()) {
				@Override
				public void success(ActionInvocation invocation) {
					if(position != 0) {
						changePosition(position);
					}

					if (autoStart) {
						start();
					} else {
						downloadService.setPlayerState(PlayerState.PAUSED);
					}

					currentPosition = position;
					lastUpdate.set(System.currentTimeMillis());
					getUpdatedStatus();
				}

				@Override
				public void failure(ActionInvocation actionInvocation, UpnpResponse upnpResponse, String msg) {
					Log.w(TAG, "Set URI failed: " + msg);
					failedLoad();
				}
			});
		} catch (Exception e) {
			Log.w(TAG, "Failed startSong", e);
			failedLoad();
		}
	}
	private void setupNextSong(final DownloadFile nextPlaying) {
		this.nextPlaying = nextPlaying;
		nextPlayingURI = null;
		if(nextPlaying == null) {
			downloadService.setNextPlayerState(PlayerState.IDLE);
			Log.i(TAG, "Nothing to play next");
			return;
		}

		downloadService.setNextPlayerState(PlayerState.PREPARING);
		try {
			Pair<String, String> songInfo = getSongInfo(nextPlaying);

			nextPlayingURI = songInfo.getFirst();
			controlPoint.execute(new SetNextAVTransportURI(getTransportService(), songInfo.getFirst(), songInfo.getSecond()) {
				@Override
				public void success(ActionInvocation invocation) {
					downloadService.setNextPlayerState(PlayerState.PREPARED);
				}

				@Override
				public void failure(ActionInvocation actionInvocation, UpnpResponse upnpResponse, String msg) {
					Log.w(TAG, "Set next URI failed: " + msg);
					nextPlayingURI = null;
					DLNAController.this.nextPlaying = null;
					downloadService.setNextPlayerState(PlayerState.IDLE);
				}
			});
		} catch (Exception e) {
			Log.w(TAG, "Failed to setup next song", e);
			nextPlayingURI = null;
			this.nextPlaying = null;
			downloadService.setNextPlayerState(PlayerState.IDLE);
		}
	}

	Pair<String, String> getSongInfo(final DownloadFile downloadFile) throws Exception {
		MusicDirectory.Entry song = downloadFile.getSong();

		// Get url for entry
		MusicService musicService = MusicServiceFactory.getMusicService(downloadService);
		String url = getStreamUrl(musicService, downloadFile);

		// Create metadata for entry
		Item track;
		if(song.isVideo()) {
			track = new VideoItem(song.getId(), song.getParent(), song.getTitle(), song.getArtist());
		} else {
			String contentType = null;
			if(song.getTranscodedContentType() != null) {
				contentType = song.getTranscodedContentType();
			} else if(song.getContentType() != null) {
				contentType = song.getContentType();
			}

			MimeType mimeType;
			// If we can parse the content type, use it instead of hard coding
			if(contentType != null && contentType.indexOf("/") != -1 && contentType.indexOf("/") != (contentType.length() - 1)) {
				String[] typeParts = contentType.split("/");
				mimeType = new MimeType(typeParts[0], typeParts[1]);
			} else {
				mimeType = new MimeType("audio", "mpeg");
			}

			Res res = new Res(mimeType, song.getSize(), url);

			if(song.getDuration() != null) {
				SimpleDateFormat df = new SimpleDateFormat("HH:mm:ss");
				df.setTimeZone(TimeZone.getTimeZone("UTC"));
				res.setDuration(df.format(new Date(song.getDuration() * 1000)));
			}

			MusicTrack musicTrack = new MusicTrack(song.getId(), song.getParent(), song.getTitle(), song.getArtist(), song.getAlbum(), song.getArtist(), res);
			musicTrack.setOriginalTrackNumber(song.getTrack());

			if(song.getCoverArt() != null) {
				String coverArt = null;
				if(proxy == null || proxy instanceof WebProxy) {
					coverArt = musicService.getCoverArtUrl(downloadService, song);

					// If proxy is going, it is a web proxy
					if(proxy != null) {
						coverArt = proxy.getPublicAddress(coverArt);
					}
				} else {
					File coverArtFile = FileUtil.getAlbumArtFile(downloadService, song);
					if(coverArtFile != null && coverArtFile.exists()) {
						coverArt = proxy.getPublicAddress(coverArtFile.getPath());
					}
				}

				if(coverArt != null) {
					DIDLObject.Property.UPNP.ALBUM_ART_URI albumArtUri = new DIDLObject.Property.UPNP.ALBUM_ART_URI(URI.create(coverArt));
					musicTrack.addProperty(albumArtUri);
				}
			}

			track = musicTrack;
		}

		DIDLParser parser = new DIDLParser();
		DIDLContent didl = new DIDLContent();
		didl.addItem(track);

		String metadata = "";
		try {
			metadata = parser.generate(didl);
		} catch(Exception e) {
			Log.w(TAG, "Metadata generation failed", e);
		}

		return new Pair<>(url, metadata);
	}

	private void failedLoad() {
		downloadService.setPlayerState(PlayerState.STOPPED);
		error = true;

		if(Looper.myLooper() != Looper.getMainLooper()) {
			downloadService.post(new Runnable() {
				@Override
				public void run() {
					Util.toast(downloadService, downloadService.getResources().getString(R.string.download_failed_to_load));
				}
			});
		} else {
			Util.toast(downloadService, downloadService.getResources().getString(R.string.download_failed_to_load));
		}
	}

	private Service getTransportService() {
		return device.renderer.findService(new ServiceType("schemas-upnp-org", "AVTransport"));
	}

	private void getUpdatedStatus() {
		// Don't care if shutdown in the meantime
		if(!running) {
			return;
		}

		controlPoint.execute(new GetPositionInfo(getTransportService()) {
			@Override
			public void received(ActionInvocation actionInvocation, PositionInfo positionInfo) {
				// Don't care if shutdown in the meantime
				if(!running) {
					return;
				}

				long duration = positionInfo.getTrackDurationSeconds();
				hasDuration = duration > 0;

				lastUpdate.set(System.currentTimeMillis());

				// Let's get the updated position
				currentPosition = (int) positionInfo.getTrackElapsedSeconds();

				if(positionInfo.getTrackURI() != null && positionInfo.getTrackURI().equals(nextPlayingURI) && downloadService.getNextPlayerState() == PlayerState.PREPARED) {
					downloadService.onNextStarted(nextPlaying);
					nextPlayingURI = null;
				}

				downloadService.postDelayed(new Runnable() {
					@Override
					public void run() {
						getUpdatedStatus();
					}
				}, STATUS_UPDATE_INTERVAL_SECONDS);
			}

			@Override
			public void failure(ActionInvocation actionInvocation, UpnpResponse upnpResponse, String s) {
				Log.w(TAG, "Failed to get an update");

				downloadService.postDelayed(new Runnable() {
					@Override
					public void run() {
						getUpdatedStatus();
					}
				}, STATUS_UPDATE_INTERVAL_SECONDS);
			}
		});
	}

	private abstract class SetNextAVTransportURI extends ActionCallback {
		public SetNextAVTransportURI(Service service, String uri) {
			this(new UnsignedIntegerFourBytes(0), service, uri, null);
		}

		public SetNextAVTransportURI(Service service, String uri, String metadata) {
			this(new UnsignedIntegerFourBytes(0), service, uri, metadata);
		}

		public SetNextAVTransportURI(UnsignedIntegerFourBytes instanceId, Service service, String uri) {
			this(instanceId, service, uri, null);
		}

		public SetNextAVTransportURI(UnsignedIntegerFourBytes instanceId, Service service, String uri, String metadata) {
			super(new ActionInvocation(service.getAction("SetNextAVTransportURI")));
			getActionInvocation().setInput("InstanceID", instanceId);
			getActionInvocation().setInput("NextURI", uri);
			getActionInvocation().setInput("NextURIMetaData", metadata);
		}
	}
}