import java.text.ParseException
import java.text.SimpleDateFormat

import org.apache.commons.lang.StringEscapeUtils
import org.serviio.library.metadata.*
import org.serviio.library.online.*
import org.serviio.util.*

/**
 * Content URL extractor plugin for BBC iPlayer (UK only) 
 *  
 * @author Petr Nejedly
 *
 */
class IPlayer extends WebResourceUrlExtractor {
    
	// https://github.com/vonH/plugin.video.iplayerwww/blob/master/default.py
	
    final VALID_FEED_URL = '^http(s)*://www\\.bbc\\.co\\.uk/iplayer/(categories|group|episodes)/.*$'
	
	@Override
    String getExtractorName() {
        return 'BBC iPlayer v2 (UK only)'
    }
    
	@Override
    boolean extractorMatches(URL feedUrl) {
        return feedUrl ==~ VALID_FEED_URL
    }
    
	@Override
    public int getVersion() {
        return 4;
    }
	
	
	@Override
	protected WebResourceContainer extractItems(URL resourceUrl, int maxFeedItemsToRetrieve) {
		String html = resourceUrl.getText()
		if(resourceUrl.toString().contains('/highlights')) {
			return parseHighlights(html, maxFeedItemsToRetrieve);
		} else {
			return parseEpisodes(html, maxFeedItemsToRetrieve);
		}
	}

	@Override
	protected ContentURLContainer extractUrl(WebResourceItem item, PreferredQuality requestedQuality) {
		String streamId = findStreamId(item.additionalInfo['videoUrl'])
		return parseStream(streamId, requestedQuality, item.additionalInfo['thumbnailUrl'])
	}

	private WebResourceContainer parseHighlights(String html, int maxFeedItemsToRetrieve) {
		
		List<WebResourceItem> items = []
		
		def innerAnchors = html.findAll(~ '(?ms)<a.*?(?!<a).*?</a>')
		def groups = innerAnchors.findAll { it =~ '(?ms)<a[^<]*?class="grouped-items__cta.*?data-object-type="group-list-link".*?' }
		
		Map<String, Integer> episodeCounts = [:]
		
		String collectionTitle = null
		def collectionTitleMatcher = html =~ '(?ms)<h1>(.*?)<'
		if(collectionTitleMatcher) {
			collectionTitle = StringEscapeUtils.unescapeHtml(collectionTitleMatcher[0][1].trim())
		}
		
		for (group in groups) {
			
			String href = null
			def hrefMatcher = group =~ '(?ms)<a[^<]*?href="(.*?)"'
			if(hrefMatcher) {
				href = hrefMatcher[0][1]
			}
			
			Integer count = null;
			def countMatcher = group =~ '(?ms)>View all ([0-9]*).*?</a>'
			if(countMatcher) {
				count = Integer.valueOf(countMatcher[0][1])
			}
			
			episodeCounts[href] = count
		}
		
		groups = innerAnchors.findAll { it =~ '(?ms)<a[^<]*?class="grouped-items__title.*?data-object-type="group-list-link".*?' }
		
		List<Group> groupProperties = []
		
		for (group in groups) {
			
			String href = null
			def hrefMatcher = group =~ '(?ms)<a[^<]*?href="(.*?)"'
			if(hrefMatcher) {
				href = hrefMatcher[0][1]
			}
			
			String name = null
			def nameMatcher = group =~ '(?ms)<strong>(.*?)</strong>'
			if(nameMatcher) {
				name = nameMatcher[0][1]
			}
			
			Integer count = episodeCounts[href]
			
			String groupType = null
			def groupTypeMatcher = html =~ '(?ms)data-group-name="'+name+'".+?data-group-type="(.+?)"'
			if(groupTypeMatcher) {
				groupType = groupTypeMatcher[0][1]
			}
			
			String position = null
			def positionMatcher = group =~ '(?ms)data-object-position="(.+?)-ALL"'
			if(positionMatcher) {
				position = positionMatcher[0][1]
			}
			
			groupProperties << new Group(name: name, position: position, type: groupType)
		}
		
		Map<String, String> episodeList = [:]
		
		def listeds = innerAnchors.findAll { it =~ '(?ms)class="grouped-items__list-link' }
		
		for( listed in listeds) {
			
			String episodeId = null
			def idMatcher = listed =~ '(?ms)<a.*?href="/iplayer/episode/(.*?)/'
			if(idMatcher) {
				episodeId = idMatcher[0][1]
			}
			
			String title = null
			def titleMatcher = listed =~ '(?ms)<.*?class="grouped-items__title.*?<strong>(.*?)</strong>'
			if(titleMatcher) {
				title = StringEscapeUtils.unescapeHtml(titleMatcher[0][1])
			}
			
			String subTitle = null
			def subTitleMatcher = listed =~ '(?ms)<.*?class="grouped-items__subtitle.*?>(.*?)<'
			if(subTitleMatcher) {
				title = title + ' - ' + subTitleMatcher[0][1]
			}
			
			String position = null
			def positionMatcher = listed =~ '(?ms)data-object-position="(.+?)"'
			if(positionMatcher) {
				
				Group relatedGroup = groupProperties.find {g -> positionMatcher[0][1].startsWith(g.position)}
				if(relatedGroup) {
					if(relatedGroup.type == 'series-catchup') {
						title = relatedGroup.name + ': ' + title 
					}
				} 				
			}
			
			episodeList[episodeId] = title
		}
		
		def singles = innerAnchors.findAll { it =~ '(?ms)class="single-item' }
		
		for( single in singles) {
			
			String objectType = null
			def objectTypeMatcher = single =~ '(?ms)data-object-type="(.*?)"'
			if(objectTypeMatcher) {
				objectType = objectTypeMatcher[0][1]
				if( objectType == 'editorial-promo') {
					continue
				}
			}
			
			String episodeId = null
			String url = null
			def idMatcher = single =~ '(?ms)<a.*?href="/iplayer/episode/(.*?)/'
			if(idMatcher) {
				episodeId = idMatcher[0][1]
				url = 'http://www.bbc.co.uk/iplayer/episode/' + episodeId
			}
			
			String title = null
			def titleMatcher = single =~ '(?ms)<.*?class="single-item__title.*?<strong>(.*?)</strong>'
			if(titleMatcher) {
				title = StringEscapeUtils.unescapeHtml(titleMatcher[0][1])
			}
			
			String subTitle = null
			def subTitleMatcher = single =~ '(?ms)<.*?class="single-item__subtitle.*?>(.*?)<'
			if(subTitleMatcher) {
				title = title + ' - ' + StringEscapeUtils.unescapeHtml(subTitleMatcher[0][1])
			}
			
			String icon = null
			def iconMatcher = single =~ '(?ms)<.*?class="r-image.*?data-ip-src="(.*?)"'
			if(iconMatcher) {
				icon = iconMatcher[0][1]
			}
			
			String synopsis = null
			def synopsisMatcher = single =~ '(?ms)<.*?class="single-item__overlay__desc.*?>(.*?)<'
			if(synopsisMatcher) {
				synopsis = StringEscapeUtils.unescapeHtml(synopsisMatcher[0][1])
			}
			
			Date releaseDate = null
			def releasedMatcher = single =~ '(?ms)<.*?class="single-item__overlay__subtitle">First shown: (.*?)<'
			if(releasedMatcher) {
				String releaseDateString = releasedMatcher[0][1]
				releaseDate = parseDate(releaseDateString)
			}
			
			Map<String, String> additionalInfo = [:]
			additionalInfo.put('thumbnailUrl', icon)
			additionalInfo.put('videoUrl', url)
			additionalInfo.put('episodeId', episodeId)
			
			items << new WebResourceItem(title: title, releaseDate: releaseDate, description: synopsis, additionalInfo: additionalInfo)
			
		}
		
		
		for( groupEpisode in episodeList ) {
			def alreadyAdded = items.find {it.additionalInfo['episodeId'] == groupEpisode.key}
			if(!alreadyAdded) {
				Map<String, String> additionalInfo = [:]
				additionalInfo.put('videoUrl', 'http://www.bbc.co.uk/iplayer/episode/' + groupEpisode.key)
				additionalInfo.put('episodeId', groupEpisode.key)
					
				items << new WebResourceItem(title: groupEpisode.value, additionalInfo: additionalInfo)
			}
		}
		
		if(maxFeedItemsToRetrieve != -1) {
			items = items.subList(0, maxFeedItemsToRetrieve);
		}
		
		return new WebResourceContainer(title: collectionTitle, thumbnailUrl: !items.empty ? items[0].additionalInfo['thumbnailUrl'] : null, items: items )
		
	}
	
	private class Group {
		public String position
		public String name
		public String type
	}
	
	private WebResourceContainer parseEpisodes(String html, int maxFeedItemsToRetrieve) {
		def currentPage = 1
		def totalPages = 1
		def pageBaseUrl = ''
		def totalItems = 0
		
		List<WebResourceItem> items = []
		
		def paginateMatcher = html =~ '(?s)<div class="paginate.*?</div>'
		if(paginateMatcher) {
			def foundPages = paginateMatcher[0].findAll(~'(?s)<li class="page.*?</li>')
			if(!foundPages.isEmpty()) {
				def lastPageMatcher = foundPages.last() =~ '(?s)<a href="(.*?page=)(.*?)">'
				if(lastPageMatcher) {
					pageBaseUrl = StringEscapeUtils.unescapeHtml(lastPageMatcher[0][1])
					totalPages = lastPageMatcher[0][2].toInteger()
				}
			}
		}
		
		String collectionTitle = null
		def collectionTitleMatcher = html =~ '(?ms)<h1>(.*?)<'
		if(collectionTitleMatcher) {
			collectionTitle = collectionTitleMatcher[0][1].trim()
		}
		
		for ( page in (1..totalPages) ) {
			if(page > currentPage) {
				if(maxFeedItemsToRetrieve != -1 && totalItems == maxFeedItemsToRetrieve) {
					break;
				}
				log("Loading page " + page)
				def url = new URL('http://www.bbc.co.uk' + pageBaseUrl + page)
				html = url.getText()
			}

			html = html.replaceAll(~'(?ms)<li data-version-type.*?</li>', '')
			def listItems = html.findAll(~'(?ms)<li class="list-item.*?</li>')
			
			for(li in listItems) {
				if(maxFeedItemsToRetrieve != -1 && totalItems == maxFeedItemsToRetrieve) {
					break;
				}
				
				if(li =~ '(?ms)<li class="list-item.*?unavailable.*?"') {
					continue;
				}
				
				String title = null
				String url = null
				String icon = null
				String synopsis = null
				Date releaseDate = null
				
				def urlMatcher = li =~ '(?ms)<a.*?href="(.*?)".*?list-item-link.*?>'
				if(urlMatcher) {
					url = 'http://www.bbc.co.uk' + urlMatcher[0][1]
				}
				def titleMatcher = li =~ '(?ms)<div class="title top-title">\\s*(.*?)\\s*</div>'
				if(titleMatcher) {
					title = StringEscapeUtils.unescapeHtml(titleMatcher[0][1])
				}
				def subTitleMatcher = li =~ '(?ms)<div class="subtitle">\\s*(.*?)\\s*</div>'
				if(subTitleMatcher) {
					title = title + ' - ' + StringEscapeUtils.unescapeHtml(subTitleMatcher[0][1])
				}
				def iconMatcher = li =~ '(?ms)<div class="r-image".+?data-ip-type="(.*?)".+?data-ip-src="http://ichef.bbci.co.uk/images/ic/336x189/(.*?)\\.jpg"'
				if(iconMatcher) {
					icon = "http://ichef.bbci.co.uk/images/ic/832x468/" + iconMatcher[0][2] + ".jpg"
				}
				def synopsisMatcher = li =~ '(?ms)<p class="synopsis">\\s*(.*?)\\s*</p>'
				if(synopsisMatcher) {
					synopsis = StringEscapeUtils.unescapeHtml(synopsisMatcher[0][1])
				}
				def releasedMatcher = li =~ '(?ms)<span class="release">.*?First shown:\\s*(.*?)\\n.*?</span>'
				if(releasedMatcher) {
					String releaseDateString = releasedMatcher[0][1]
					releaseDate = parseDate(releaseDateString)
				}
				
				totalItems = totalItems + 1
				Map<String, String> additionalInfo = [:]
				additionalInfo.put('thumbnailUrl', icon)
				additionalInfo.put('videoUrl', url)
				
				items << new WebResourceItem(title: title, releaseDate: releaseDate, description: synopsis,additionalInfo: additionalInfo)
			}
			
		}
	
		return new WebResourceContainer(title: collectionTitle, thumbnailUrl: !items.empty ? items[0].additionalInfo['thumbnailUrl'] : null, items: items )
	}
		
	private def parseDate(String date) {
		try {
			return new SimpleDateFormat('dd MMM yyyy').parse(date)
		} catch (ParseException e) {
			return new SimpleDateFormat('dd MMM yyyy').parse("1 Jan $date")
		}
	}
	private def findStreamId(String videoUrl) {
		String html =  new URL(videoUrl).getText()
		
		def streamIdMatcher = html =~ '(?s)"vpid":"(.+?)"'
		if(streamIdMatcher) {
			return streamIdMatcher[0][1]
		} else {
			throw new RuntimeException("Cannot find stream id")
		}
	}
	
	private def ContentURLContainer parseStream(String streamId, PreferredQuality requestedQuality, String thumbnailUrl) {
		URL descriptorUrl = new URL("http://open.live.bbc.co.uk/mediaselector/5/select/version/2.0/mediaset/iptv-all/vpid/" + streamId)

		def mediaSelectionNode = new XmlParser().parseText( descriptorUrl.getText() )
		
		// get all media items that are either video or audio and have a supported connection sub-element
		List mediaItems = mediaSelectionNode.media.findAll { it -> (it.'@kind' == 'video' || it.'@kind' == 'audio') && it.connection.any { item -> item.'@supplier' == 'akamai_hls_open'} }
		
		if(mediaItems.size() > 0) {
			// sort media items by bitrate, lowest first and get an item
			List sortedItems = mediaItems.sort { it.'@bitrate'.toInteger() }
			Node selectedMediaItem = findSuitableItem(sortedItems, requestedQuality)
			// find first suitable connection element
			Node connectionNode = selectedMediaItem.find{ it -> it.'@supplier' == 'akamai_hls_open' }
			if( connectionNode != null ) {
				String contentUrl = connectionNode.'@href'
				
				MediaFileType fileType = MediaFileType.VIDEO;
				if( selectedMediaItem.'@kind'.startsWith('audio')) {
					fileType = MediaFileType.AUDIO;
				}
				Date itemExpiryDate = getExpiresGMTDate(selectedMediaItem.'@expires')
				String cacheKey = streamId + '_' + selectedMediaItem.'@bitrate' + '_' + connectionNode.'@supplier'
				return new ContentURLContainer(fileType: fileType, contentUrl: contentUrl, thumbnailUrl: thumbnailUrl, expiresOn: itemExpiryDate, cacheKey: cacheKey, live: false)
			}
		}
		return null;
	}
	
	private def Date getExpiresGMTDate(String dateString) {
		// expiry is in format 2016-02-26T16:30:00Z , let's assume it's always GMT
		SimpleDateFormat df = new SimpleDateFormat('yyyy-MM-dd\'T\'HH:mm:ss')
		df.setTimeZone(TimeZone.getTimeZone("GMT"))
		return df.parse(dateString)
	}
	
	private def Node findSuitableItem(List items, PreferredQuality requestedQuality) {
		if(requestedQuality == PreferredQuality.LOW || items.size() <= 1) {
			// worst quality, get the first from the list
			return items.head()
		} else if (requestedQuality == PreferredQuality.MEDIUM) {
			// get item from the middle
			return items.get(Math.round(items.size()/2).toInteger())
		} else {
			// best quality, take the last url
			return items.last()
		}
	}
    
	
    static void main(args) {
		// this is just to test
        IPlayer extractor = new IPlayer()
		
		// categories
		// http://www.bbc.co.uk/iplayer/categories/documentaries/highlights
		// http://www.bbc.co.uk/iplayer/categories/documentaries-history/highlights
		
		// groups / collections
		// http://www.bbc.co.uk/iplayer/group/b06z98k4
		// http://www.bbc.co.uk/iplayer/group/p02j4np8
		
		// episodes (id comes from the programme URL)
		// http://www.bbc.co.uk/iplayer/episodes/b006m86d
		
		assert extractor.extractorMatches( new URL("http://www.bbc.co.uk/iplayer/categories/documentaries/highlights") )
		assert extractor.extractorMatches( new URL("http://www.bbc.co.uk/iplayer/categories/documentaries-history/highlights") )
		assert extractor.extractorMatches( new URL("http://www.bbc.co.uk/iplayer/group/b06z98k4") )
		assert extractor.extractorMatches( new URL("http://www.bbc.co.uk/iplayer/group/p02j4np8") )	
		assert extractor.extractorMatches( new URL("http://www.bbc.co.uk/iplayer/episodes/b006m86d") )
		
		
		//WebResourceContainer container = extractor.extractItems( new URL("http://www.bbc.co.uk/iplayer/group/b03m36wd"), -1)
		// WebResourceContainer container = extractor.extractItems( new URL("http://www.bbc.co.uk/iplayer/episodes/b006m86d"), -1)
		//WebResourceContainer container = extractor.extractItems( new URL("http://www.bbc.co.uk/iplayer/categories/documentaries-history/highlights"), -1)
		WebResourceContainer container = extractor.extractItems( new URL("http://www.bbc.co.uk/iplayer/categories/music/all?sort=atoz"), -1)
		 
		 
		//WebResourceContainer container = extractor.extractItems( new URL("http://www.bbc.co.uk/iplayer/categories/news/all?sort=atoz"), 3)
		
		println container
		ContentURLContainer result = extractor.extractUrl(container.getItems()[2], PreferredQuality.MEDIUM)
		println result
		 
    }

}