/*
 * PS3 Media Server, for streaming any medias to your PS3.
 * Copyright (C) 2008  A.Brochard
 *
 * This program 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; version 2
 * of the License only.
 *
 * This program 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 this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */
package net.pms.dlna;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;

import net.pms.Messages;
import net.pms.PMS;
import net.pms.configuration.MapFileConfiguration;
import net.pms.configuration.PmsConfiguration;
import net.pms.configuration.RendererConfiguration;
import net.pms.external.AdditionalFolderAtRoot;
import net.pms.external.AdditionalFoldersAtRoot;
import net.pms.external.ExternalFactory;
import net.pms.external.ExternalListener;
import net.pms.gui.IFrame;
import net.pms.xmlwise.Plist;
import net.pms.xmlwise.XmlParseException;
import net.pms.dlna.MapFile;			//regzamod
import net.pms.dlna.virtual.VirtualFolder;
import net.pms.dlna.virtual.VirtualVideoAction;
import net.pms.dlna.virtual.MediaLibrary;

import net.pms.dlna.rz_VirtualVideoActionF;	//regzamod
import net.pms.dlna.rz_ScriptMenuFolder;	//regzamod
import net.pms.dlna.rz_WebQSFolder;			//regzamod
import net.pms.dlna.rz_ResumeFolder;		//regzamod
import net.pms.dlna.rz_WatchedFolder;		//regzamod
import net.pms.dlna.rz_IavSelCtl;			//regzamod
import net.pms.dlna.rz_ItunesFolder;		//regzamod
import net.pms.util.PMSUtil;
import net.pms.util.FileUtil;

import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.sun.jna.Platform;

public class RootFolder extends DLNAResource {
	private static final Logger logger = LoggerFactory.getLogger(RootFolder.class);
	private PmsConfiguration configuration = PMS.getConfiguration();
	private boolean running;
	private rz_WebQSFolder rz_WebQS=null;	//regzamod, test
	private int menu_type=PMS.getConfiguration().getRZ_menu_type();
	private rz_ResumeFolder resFolderBase;  //body of resumeFolder, that refered from child folders
	private rz_WatchedFolder watchedFolderBase;  //body of resumeFolder, that refered from child folders
	private rz_IavSelCtl iavSelCtl; 
	private rz_KeywordMenu keywdMenuBase;
	private DLNAResource videoSettingsRes;
	private long prevCallTime;
	private int btn_type;
	private boolean rz_MenuLockEnabled;
	private int rz_MenuLock_sv;	//memory
	public int rz_MenuLock;   	//block unintended menu-operations from "auto-search type renderer" like LG SmartTV, that crawls all menu folders

	// List is an interface(abstruct-side-super-class), ArrayList is an implementation of List
	private List<String> drives=new ArrayList<String>();   //list of DriveLetters that drive-children(FileSystem's root) have
	
	public RootFolder() {
		construct(null);
		/*
		setIndexId(0);
		sort_enabled=false;
		doSort=true;
		rz_MetaSortType=PMS.SORT_ORIGIN;
		*/
	}
	public RootFolder(RendererConfiguration r, rz_SessionInfo sess) {
		setCurrentSession(sess);
		construct(r);
		if(PMS.rz_debug>1) { 
			PMS.dbg("RootFolder:"
				+" renderer="+r
				+", sess="+sess
				+", rz_MenuLockEnabled="+rz_MenuLockEnabled
			);
		}
	}
	
	private void construct(RendererConfiguration r) {
		setIndexId(0);
		sort_enabled=true;
		doSort=true;
		rz_MetaSortType=PMS.SORT_ORIGIN;
		this.setDefaultRenderer(r);
		//PMS.dbg("RootFolder.construct: getDefaultRenderer="+getDefaultRenderer());
		
		rz_MenuLock=configuration.getRZ_auto_search_lock();  //system default
		int lk=r.getRZ_AutoSearchLock();	//per renderer define
		if(lk>=0) rz_MenuLock=lk;
		
		rz_MenuLock_sv=rz_MenuLock; //memory
		rz_MenuLockEnabled=false;
		if(rz_MenuLock!=0) {
			rz_MenuLockEnabled=true;
		}
	}
	
	public DLNAResource getWebQSFolder() {	//regzamod
		//call me: PMS.get().getRootFolder(RendererConfiguration renderer).getWebQSFolder()
		return rz_WebQS;
	}
	
	public rz_KeywordMenu getKwdMenuBase() {	//regzamod
		if(rz_WebQS==null) return null;
		return rz_WebQS.getKeywordMenu();
	}

	@Override
	public InputStream getInputStream() {
		return null;
	}

	@Override
	public String getName() {
		return "root";
	}
	@Override
	public String getDpName() {	//regzamod
		return getName();
	}

	@Override
	public boolean isFolder() {
		return true;
	}

	@Override
	public long length() {
		return 0;
	}

	@Override
	public String getSystemName() {
		return getName();
	}

	@Override
	public boolean isValid() {
		return true;
	}

	@Override
	public int activate(int type) {
		//PMS.dbg("RootFolder.activate: type="+type+", rz_MenuLockEnabled="+rz_MenuLockEnabled
		//	+", rz_MenuLock="+rz_MenuLock
		//	+", rz_MenuLock_sv="+rz_MenuLock_sv);
		
		long tim1 = System.currentTimeMillis();
		if(type!=1 || (tim1-prevCallTime) <1000) { 
			//PMS.dbg("RootFolder.activate: called in short time --> Noop, renderer="+getDefaultRenderer());
			// type=1: indicate Folder Open action from renderer, called 2 times on one open why??
			return 0;
		}
		int rc=activate_in(type);
		prevCallTime=tim1;
		return rc;
	}
		
	private int activate_in(int type) {
		if(true || rz_MenuLockEnabled) {  //logic of Check rz_MenuLockEnabled is something wrong: if not true, cause Medialibraly is hidden
			if(rz_MenuLock==0) {  //unlock
				if(videoSettingsRes!=null) videoSettingsRes.mflags&=~MF_HIDE;
				if(rz_WebQS!=null) rz_WebQS.mflags&=~MF_HIDE;
				if(resumeFolder!=null) resumeFolder.mflags&=~MF_HIDE;
				for(DLNAResource d : getChildren()) {
					if((d.mflags&MF_HIDE_SYS)==0) {
						d.mflags&=~MF_HIDE;
					}
				}
			}
			else if(rz_MenuLock==1) {  //lock web/resume/settings
				if(videoSettingsRes!=null) videoSettingsRes.mflags|=MF_HIDE;
				if(rz_WebQS!=null) rz_WebQS.mflags|=MF_HIDE;
				if(resumeFolder!=null) resumeFolder.mflags|=MF_HIDE;
				for(DLNAResource d : getChildren()) {
					if((d.mflags & DLNAResource.MF_WEB_FOLDER)!=0) {
						d.mflags|=MF_HIDE;
					}
				}
			}
			else if(rz_MenuLock==2) {  //lock all 
				//PMS.dbg("RootFolder.activate: rz_MenuLock=2, renderer="+getDefaultRenderer());
				for(DLNAResource d : getChildren()) {
					if(d.sotype!=SO_LOCK_BTN) {
						//PMS.dbg("RootFolder: Set DoLock Child="+d.getName());
						d.mflags|=MF_HIDE;
					}
					else {
						//PMS.dbg("RootFolder: Set UnLock Child="+d.getName());
					}
				}
			}
		}
		return 0;
	}

	@Override
	public void discoverChildren() {
		RendererConfiguration rend=getDefaultRenderer();
		
		if(PMS.rz_debug>1) {
			PMS.dbg("RoorFolder.discoverChildren: start"
				+", rend="+getDefaultRenderer()
				+", cur sess="+getCurrentSession()
				+", rend sess="+(rend==null?null:rend.sess)
				+", isDiscovered()="+isDiscovered()
			);
		}
		//PMS.dbg("RootFolder.discoverChildren: getDefaultRenderer="+getDefaultRenderer());
		if(isDiscovered()) {
			logger.warn("RoorFolder.discoverChildren: Already discovered --> Noop");
			try {
				Thread.sleep(3*1000);  //msec
			} catch (InterruptedException e) {
			}
			return;
		}
		setDiscovered(true);  //add, regzam to avoid secondly running before first finishes
		setCurrentSession(rend.sess);
		
		//action button type
		btn_type=configuration.getRZ_action_button_type();  //system define(default)
		int bt=getDefaultRenderer().getRZ_ActionButtonType();  //per renderer define
		if(bt>=0) btn_type=bt;
		
		//------------------------------------------------------------------------
		// create menus under MenuFolder 
		//------------------------------------------------------------------------
		//---- VideoSetting Folder, must be created before webFolder
		if(!configuration.getHideVideoSettings()) {
			videoSettingsRes = getVideoSettingssFolder();
			if(videoSettingsRes != null) {
				videoSettingsRes.setSerialId(DLNAResource.MENU_ORD_VIDEO_SETTING); //force order under menuFolder
				if(menu_type>=2) {
					VirtualFolder vf=getCtlFolder(true,-1);
					vf.addChildInternal(videoSettingsRes);
				}
				else {
					addChildInternal(videoSettingsRes);
				}
			}
		}

		//---- regzamod, resume/watched folder
		if(PMS.rz_resume_mode!=0) {
			//---- regzamod, resume folder
			if(resFolderBase==null) {
				if(PMS.get().rz_resume!=null) {
					resFolderBase=new rz_ResumeFolder(PMS.get().rz_resume.getBaseFolder(),"#- RESUME ALL -#",false);
				}
			}
			if(resFolderBase != null) {
				if(menu_type>=2) {
					VirtualFolder vf=getCtlFolder(true,-1);
					//make controllFolder to be almost last of list
					vf.setSerialId(2000);
					vf.sort_immune=0;  //reset default setting(true) for controll folder
					vf.addChildInternal(resFolderBase);
					vf.resumeFolder=resFolderBase;
				}
				else {
					addChildInternal(resFolderBase);
				}
				resumeFolder=resFolderBase;  //see DLNAResource.createResumeFolder
				resumeFolder.isResumeFolderAll=true;
				resumeFolder.setSerialId(DLNAResource.MENU_ORD_RESUME_ALL); //force order under menuFolder
				//PMS.dbg("RootFolder: add resumeFolder id="+resFolderBase.getInternalId());
			}
			PMS.dbg("RootFolder: after resumefolder: getLastChildrenId()="+getLastChildrenId());
			
			//---- watched folder
			if(watchedFolderBase==null && resFolderBase!=null) {
				watchedFolderBase=new rz_WatchedFolder(PMS.get().rz_resume.getWatchedFolder(), "#- WATCHED -#", false);
				if(watchedFolderBase!=null) {
					resFolderBase.addChild(watchedFolderBase);
					//resFolderBase.informWatchedFolder(watchedFolderBase);
				}
			}
		}

		//---- regzam, selection folder for sound/visual clippath 
		if(iavSelCtl==null) {
			//PMS.dbg("RootFolder: Create selClippath folder Start");
			long tm1=System.currentTimeMillis();
			if(menu_type>=2) {
				VirtualFolder vf=getCtlFolder(true,-1);
				vf.iavSelCtl=new rz_IavSelCtl(vf,this);
				iavSelCtl=vf.iavSelCtl;
			}
			else {
				iavSelCtl=new rz_IavSelCtl(this,this);
			}
			iavSelCtl.getSelRoot().setSerialId(DLNAResource.MENU_ORD_BG_CLIP);  //force order under menuFolder
			DLNAResource bd=iavSelCtl.getSelRoot();
			for(DLNAResource ch : bd.getChildren()) {
				ch.resolve();
			}
			long tm2=System.currentTimeMillis();
			//PMS.dbg("RootFolder: Create selClippath folder End, ElapsedTime(sec)="+(tm2-tm1)/1000.0);
		}
		
		//---- User Root Folders ---------------
		for (DLNAResource r : getConfiguredFolders()) {
			addChild(r);
		}
		for (DLNAResource r : getVirtualFolders()) {
			addChild(r);
		}
		
		//---- WEB Folder ---------------
		DLNAResource WebRoot=null;
		File webConf = new File(configuration.getProfileDirectory(), "WEB.conf");
		if (webConf.exists()) {
			WebRoot=addWebFolder(webConf,this);
			//PMS.dbg("RootFolder: WebRoot="+WebRoot);
		}
		
		//---- WEB_QS Folder ---------------
		//regzamod
		File qsConf = new File(configuration.getProfileDirectory(), "WEB_QS.conf");
		if (qsConf.exists()) {
			if(WebRoot!=null) {
				rz_WebQS=new rz_WebQSFolder(WebRoot,"[WebQS]",btn_type);
				rz_WebQS.setSerialId(1);
				WebRoot.addChild(rz_WebQS);
			}
			else {
				rz_WebQS=new rz_WebQSFolder(this,"WebQS",btn_type);
				addChild(rz_WebQS);
			}
		}
		
		//---- iPhoto Folder ---------------
		if (Platform.isMac() && configuration.getIphotoEnabled()) {
			DLNAResource iPhotoRes = getiPhotoFolder();
			if (iPhotoRes != null) {
				addChild(iPhotoRes);
			}
		}
		
		//---- iAperture Folder ---------------
		if (Platform.isMac() && configuration.getApertureEnabled()) {
			DLNAResource apertureRes = getApertureFolder();
			if (apertureRes != null) {
				addChild(apertureRes);
			}
		}
		//---- iTunesFolder ---------------
		if ((Platform.isMac() || Platform.isWindows()) && configuration.getItunesEnabled()) {
			//DLNAResource iTunesRes = getiTunesFolder();
			int pmode=configuration.getRZ_itunes_parse_mode();
			int pmode_r=rend.getRZ_ItunesParseMode();
			if(pmode_r>=0) pmode=pmode_r; //override
			
			if(rz_MenuLock>0) {
				pmode=0;  //delay open
			}
			PMS.dbg("RootFolder: rend="+getDefaultRenderer()+", call getiTunesFolder, pmode="+pmode+" (=0:delay open,=1:open now)");
			DLNAResource iTunesRes = getiTunesFolder(pmode);	//regzamod
			if (iTunesRes != null) {
				addChild(iTunesRes);
				//PMS.dbg("RootFolder: my renderer="+getDefaultRenderer());
				//PMS.dbg("RootFolder: create iTunes top_folder, renderer="+iTunesRes.getDefaultRenderer());
			}
		}
		
		//---- MediaLibraryFolder ---------------
		if (configuration.getUseCache() && !configuration.isHideMediaLibraryFolder()) {
			if(true) {  //create per rootFolder
				//PMS.dbg("RootFolder: create MediaLibraryFolder Start, renderer="+getDefaultRenderer());
				DLNAResource libraryRes = new MediaLibrary(getDefaultRenderer());
				addChild(libraryRes);
				//PMS.dbg("RootFolder: create MediaLibraryFolder End, renderer="+getDefaultRenderer());
			}
			else {  //create per PMS: can't support difference of renderer.confs
				DLNAResource libraryRes = PMS.get().getLibrary();
				//PMS.dbg("RootFolder: libraryRes="+libraryRes);
				if (libraryRes != null) {
					//NOT to make direct Child , but linked child
					if(true) {
						VirtualFolder vf = new VirtualFolder(libraryRes.getName(), null);
						addChild(vf);
						//vf.rz_linkobj=libraryRes;
						for(DLNAResource d: libraryRes.getChildren()) {
							vf.addChild(d);
						}
						//libraryRes.discoverChildren();
						//libraryRes.resolve();
					}
					else {
						//if Onject Id is not Null, children's Id will be not count-Upped in addChild()
						//so, will cause Id Conflicts after. 
						libraryRes.setId(null); //assert id is NULL(why not null??)
						addChild(libraryRes);
					}
				}
			}
		}
		PMS.dbg("RootFolder: After MediaLibraryFolder: getLastChildrenId()="+getLastChildrenId());
		
		//---- Plug-In's RootFolders ---------------
		for (DLNAResource r : getAdditionalFoldersAtRoot()) {
			addChild(r);
		}
		
		//---------------------
		//PMS.dbg("RootFolder: after TestFolder1: getLastChildrenId()="+getLastChildrenId());
		
		//---- regzamod, script menu folder
		if(false) {
			//PMS.dbg("RootFolder: create ScriptMenu: rend="+getDefaultRenderer());
			//RootFolder is not asigned to specific renderer yet
			rz_ScriptMenuFolder ch=new rz_ScriptMenuFolder("#- SCRIPT MENU -#",null,getDefaultRenderer());
			if(menu_type>=2) {
				VirtualFolder vf=getCtlFolder(true,-1);
				vf.addChildInternal(ch);
			}
			else {
				addChildInternal(ch);
			}
		}
		
		//disp menues, even if only folders
		if(menu_type>=2) {  
			//to be refined: createClearFolderCahceMenu cause error for root
			createSortMenu(this,-1);
			createScriptMenuFolder(this,-1);
			createClearFolderCahceMenu(this,-1);
			createSelClipPath(this,-1);
			//createVideoSettingMenu(this);
		}
		
		/* moved upper
		//---- VideoSetting Folder
		if(!configuration.getHideVideoSettings()) {
			videoSettingsRes = getVideoSettingssFolder();
			if(videoSettingsRes != null) {
				if(menu_type>=2) {
					VirtualFolder vf=getCtlFolder(true,-1);
					vf.addChildInternal(videoSettingsRes);
				}
				else {
					addChildInternal(videoSettingsRes);
				}
			}
		}
		*/
		
		//---- MenuLock Folder
		if(rz_MenuLockEnabled) {
			DLNAResource d=AutoSearchLockBtn();  //rz_MenuLock
			if(rz_MenuLock_sv==2) {  //lock all button : must be displayed directry under root
				VirtualFolder vf=new VirtualFolder("#- DispLock Button -#",null);
				vf.sotype=DLNAResource.SO_LOCK_BTN;
				//make controllFolder to be last of list
				vf.setSerialId(1002);
				//vf.sort_immune=0;
				addChildInternal(vf);
				vf.addChild(d);
			}
			else if(menu_type>=2) {  // disp under controll folder
				VirtualFolder vf=getCtlFolder(true,-1);
				vf.addChild(d);
			}
			else {
				addChild(d);
			}
		}
		
		//---- Test folders
		/*
		vf0=new VirtualFolder("TestFolder2", null);
		addChildInternal(vf0);
		vf1=new VirtualFolder("TestFolder2-SubFolder1", null);
		vf0.addChildInternal(vf1);
		vf1=new VirtualFolder("TestFolder2-SubFolder2", null);
		vf0.addChildInternal(vf1);
		*/
		//---------------------
		setDiscovered(true);
		//PMS.dbg("RootFolder.DiscoverChildren: call activate_in(1)");
		activate_in(1);
		
		//---- listup DriveLettrs of drive-children
		if(Platform.isWindows()) {
			for(DLNAResource ch: getChildren()) {
				if(!ch.isRealFile()) continue; 
				File f=ch.getFile();
				if(f==null) continue; //who knows?
				if(f.getPath().matches("[A-Z]:.")) {
					drives.add(f.getPath());
					if(PMS.rz_debug>1) PMS.dbg("RootFolder: found drive-child="+ch.getName()+", DriveLetter="+ f.getPath());
				}
			}
		}
	}
	
	public boolean isOnSlowMedia(DLNAResource dlna) { //regzam
		if(!dlna.isRealFile()) return false;
		if(!Platform.isWindows()) return false;//who knows?
		File df=dlna.getFile();
		if(df==null || df.getPath()==null) return false;
		String dpath=df.getPath();
		for(String dls: drives) {
			if(dpath.startsWith(dls)) {  // under drive-children
				return true;
			}
		}
		return false;
	}

	public VirtualFolder getResumeFolderVf(DLNAResource pa, String name) {
		if(resFolderBase==null) {
			return null;
		}
		VirtualFolder vf = new VirtualFolder(name, null);
		vf.rz_linkobj=resFolderBase;
		return vf;
	}
	
	public rz_IavSelCtl getSelClipBase() {
		return iavSelCtl;
	}
	
	public String getCurrentClipPath_a() {
		if(iavSelCtl==null) return null;
		return iavSelCtl.getCurrentClipPath_a();
	}
	public String getCurrentClipPath_v() {
		if(iavSelCtl==null) return null;
		return iavSelCtl.getCurrentClipPath_v();
	}
	
	public void getSelClipPathVf(DLNAResource pa, String name, int mode) {
		if(iavSelCtl==null) {
			return ;
		}
		if(pa.iavSelCtl!=null) {
			return ;
		}
		pa.iavSelCtl=iavSelCtl;
		VirtualFolder vf;
		DLNAResource bd=iavSelCtl.getSelRoot();
		
		if(mode==0) {
			vf= new VirtualFolder("#- BackGround Clips -#", null);
			vf.rz_linkobj=bd;
			pa.addChildInternal(vf);
		}
		else {
			for(DLNAResource ch : bd.getChildren()) {
				ch.resolve();
				vf= new VirtualFolder(ch.getName(), null);
				vf.rz_linkobj=ch;
				pa.addChildInternal(vf);
			}
		}
	}
	
	//---- return dummy folder that have subfolders, for disp multi-page when list over max
	private VirtualFolder mpvf0,mpvf1;
	private int mpvf0_cnt;
	private int list_cnt_sv,page_max_sv;
	DLNAResource pa_sv;
	public DLNAResource getMultiPageFolder(DLNAResource pa,int list_cnt,int page_max) {
		//PMS.dbg("getMultiPageFolder: list_cnt="+list_cnt+", page_max="+page_max);
		int sf_cnt=(int)Math.ceil(list_cnt/((float)page_max));
		
		//---- hidden multi-page body (folder + subfolders_per page_range)
		if(mpvf0==null) {
			mpvf0=new VirtualFolder("MultiPageFolder",null);
			addChildInternal(mpvf0);
			mpvf0.mflags|=(MF_HIDE|MF_HIDE_SYS);
		}
		if(sf_cnt>mpvf0_cnt) {
			for(int i=mpvf0_cnt;i<sf_cnt;i++) {
				VirtualFolder vf=new VirtualFolder("page-"+i,null);
				vf.sort_enabled=false;
				mpvf0.addChildInternal(vf);
				mpvf0.setIndexId_Subp(i);
				mpvf0_cnt++;
			}
		}
		
		//---- virtual link to multi-page body (folder + subfolders_per page_range)
		if(mpvf1==null) {
			mpvf1=new VirtualFolder("MultiPageFolder",null);
			addChildInternal(mpvf1);
			mpvf1.mflags|=(MF_HIDE|MF_HIDE_SYS);
		}
		if(list_cnt==list_cnt_sv && page_max== page_max_sv && pa==pa_sv) {
			return mpvf1;
		}
		mpvf1.getChildren().clear();
		mpvf1.rz_srcobj=pa;
		int stp,enp;
		for(int i=0;i<sf_cnt;i++) {
			DLNAResource d=mpvf0.getChildren().get(i);
			d.setIndexId_Subp(i);
			stp=page_max*i+1;
			if(i==sf_cnt-1) {
				enp=list_cnt;
			}
			else {
				enp=page_max*(i+1);
			}
			d.setName("[ "+stp+" - "+enp+" ]");
			mpvf1.addChildInternal(d);
		}
		list_cnt_sv=list_cnt;
		page_max_sv=page_max;
		pa_sv=pa;
		return mpvf1;
	}

	@Override
	public void refreshChildren() {	//regzamod
		//PMS.dbg("RootFolder.refreshChildren: called");
	}	

	private int g_scnt;
	
	public void scan() {
		//this is dummy root
		running = true;
		if(!isDiscovered()) {
			discoverChildren();
		}
		
		logger.info("==== Scan Start ===========");
		g_scnt=0;
		long tim0 = System.currentTimeMillis();
		
		RendererConfiguration r=RendererConfiguration.getDefaultConf();
		setDefaultRenderer(r);
		//use ParseMode same as current, so don't force ParseModeV2
		if(PMS.rz_debug>1) {
			PMS.dbg("scan: Started, renderer="+r.getRendererName()
				+", isMediaParserV2="+r.isMediaParserV2()
				+", getRZ_MediaParseType="+r.getRZ_MediaParseType());
		}
		
		PMS.isMediaCacheClearRunning=true;  //to avoid dlna's addchild()->isValid() to call resolve() directly
		scan(this);
		PMS.isMediaCacheClearRunning=false;
		
		long tim1 = System.currentTimeMillis();
		
		IFrame frame = PMS.get().getFrame();
		frame.setScanLibraryEnabled(true);
		PMS.get().getDatabase().cleanup();  //claer gabage (not existing entries)
		frame.setStatusLine(null);
		
		long tim2 = System.currentTimeMillis();
		
		logger.info("MediaLibScan: scan_time(sec)="+(tim1-tim0)/1000.0+", scan_files(count)="+g_scnt);
		logger.info("  db_cleanup_time(sec)="+(tim2-tim1)/1000.0+", total_elapsed_time(sec)="+(tim2-tim0)/1000.0);
		logger.info("==== Scan End ============");
	}

	public void stopscan() {
		running = false;
	}

	private synchronized void scan (DLNAResource resource) {
		//perpose is update/create DataBase, so createing DLNA folder tree and children are 
		//only for call resolve(), after resolved, DLNA folder tree and children are never used --> to be cleared.
		
		if (running) {
			for (DLNAResource child : resource.getChildren()) {
				if (running && child.allowScan()) {	//child is Node(Folder) 
					child.setDefaultRenderer(resource.getDefaultRenderer());
					/* moved under
					String trace = null;
					if (child instanceof RealFile) {
						//trace = "Scanning Folder: " + child.getName() + ", renderer="+child.getDefaultRenderer().getRendererName();
						trace = "Scanning Folder: " + child.getSrcPath();
					}
					if (trace != null) {
						logger.debug(trace);
						PMS.get().getFrame().setStatusLine(trace);
					}
					*/
					if (child.isDiscovered()) {
						child.refreshChildren();
					} else {
						if (child instanceof DVDISOFile || child instanceof DVDISOTitle) // ugly hack
						{
							child.resolve();
						}
						child.discoverChildren();
						child.analyzeChildren(-1);
						child.setDiscovered(true);
					}
					int count = child.getChildren().size();
					if (count == 0) {
						continue;
					}
					scan(child);
					child.getChildren().clear(); //under dummy root:so not-used --> clear to spare memories
				}
			}
			
			//children may changed(deleted/added), so re-get children
			if (running && resource.allowScan()) {
				//trace = "Scanning Folder: " + child.getName() + ", renderer="+child.getDefaultRenderer().getRendererName();
				String trace = "Scanning Folder: " + resource.getSrcPath();
				logger.debug(trace);
				PMS.get().getFrame().setStatusLine(trace);
				
				for (DLNAResource child : resource.getChildren()) {
					if (running && !child.isFolder() && !child.allowScan()) {	//may be Leaf(files)
						if(true || !child.resolved) {
							//child.resolve() shoud be allways called, to check if according entry exists in DataBase 
							//and checking child.resolved has no meaning.
							//b/c child is always NOT resolved (cleared by every scan! see above [child.getChildren().clear()])
							
							//PMS.dbg("child Exec resolve, name="+child.getName());
							child.resolve();  //re-resolve immedietely
							g_scnt++;
						}
						else {
							//never comes here
							//PMS.dbg("child Already resolved, name="+child.getName());
						}
					}
				}
			}
		}
	}
	
	private synchronized void scan_old (DLNAResource resource) {
		if (running) {
			for (DLNAResource child : resource.getChildren()) {
				if (running && child.allowScan()) {	//child is Node(Folder) 
					child.setDefaultRenderer(resource.getDefaultRenderer());
					String trace = null;
					if (child instanceof RealFile) {
						trace = "Scanning Folder: " + child.getName();
					}
					if (trace != null) {
						logger.debug(trace);
						PMS.get().getFrame().setStatusLine(trace);
					}
					if (child.isDiscovered()) {
						child.refreshChildren();
					} else {
						if (child instanceof DVDISOFile || child instanceof DVDISOTitle) // ugly hack
						{
							child.resolve();
						}
						child.discoverChildren();
						child.analyzeChildren(-1);
						child.setDiscovered(true);
					}
					int count = child.getChildren().size();
					if (count == 0) {
						continue;
					}
					scan(child);
					child.getChildren().clear();
				}
			}
		}
	}
	
	public static void ClearFolderChache(DLNAResource folder,int mode) {	//regzamod
		//mode==1: clear only files direct under the folder
		//mode==2: clear all files & subfolders recursively
		long tm1=System.currentTimeMillis();
		long scanid=tm1;
		String msg;
		
		PMS.isMediaCacheClearRunning=true;
		
		msg="ClearFolderCache: started, folder="+folder.getName()+", mode="+mode;
		logger.debug(msg);
		logger.debug("  mode==1: clear MediaCache for only files direct under the folder");
		logger.debug("  mode==2: clear MediaCache for all files & subfolders recursively");
		PMS.get().getFrame().setStatusLine(msg);

		FileUtil.ClearSubtitlesCache();
		
		DLNAMediaDatabase db=null;
		if(PMS.getConfiguration().getUseCache()) {
		 	db = PMS.get().getDatabase();
		}
		ClearFolderChache_in(db,folder,mode,scanid,0);
		
		PMS.isMediaCacheClearRunning=false;
		
		long tm2=System.currentTimeMillis();
		msg="ClearFolderCache end, Folder="+folder.getName()+", elapsed time(sec)="+((tm2-tm1)/1000.0);
		logger.debug(msg);
		PMS.get().getFrame().setStatusLine(msg);

	}
	private static synchronized void ClearFolderChache_in(DLNAMediaDatabase db, DLNAResource resource
		,int mode,long scanid, int layer) {
		if(layer>15) {
			logger.warn("ClearFolderChache_in: reached too deep layer="+layer+", return to upper");
			return;
		}
		if(true) {
			String path=null;
			int[] rc=new int[1];
			
			if(resource.scanid==scanid) {
				//already scaned? 
				logger.warn("ClearFolderChache_in: found already scaned object="+resource);
				logger.warn("ClearFolderChache_in: get back, to avoid infinite loop");
				return;
			}
			resource.scanid=scanid;
			resource.setResolved(false);
			
			for (DLNAResource child : resource.getChildren()) {	//regzamod
				if (child.allowScan()) {  //folder of class MapFile
					//child.setDefaultRenderer(resource.getDefaultRenderer());
					String trace = null;
					//if (child instanceof RealFile) {
					if (child.isFolder()) {
						trace = "Clearing Folder Cache: " + child.getName();
					}
					if (trace != null) {
						logger.debug(trace);
						PMS.get().getFrame().setStatusLine(trace); //slow!!
					}
					if (child.isDiscovered()) {
						child.refreshChildren();
					} else {
						if (child instanceof DVDISOFile || child instanceof DVDISOTitle) // ugly hack
						{
							//child.resolve();
						}
						child.discoverChildren();
						child.analyzeChildren(-1);
						child.setDiscovered(true);
					}
					int count = child.getChildren().size();
					if (count == 0) {
						continue;
					}
					if(mode==2) { // clear subfolders recursively
						ClearFolderChache_in(db,child,mode,scanid,layer+1);
						//child.getChildren().clear();
					}
				}
				else { //may be file
					rc[0]=0;
					boolean drc=true;
					path=child.getMediaCachePath(rc);
					if(PMS.rz_debug>1) PMS.dbg("ClearFolderChache_in: CkearTarget name="+child.getName()+", path="+path+", db="+db);
					if(path!=null && db!=null) {
						if(rc[0]==0) {
							drc=db.delData(path,1); //clear MediaCache in db
						}
						else if(rc[0]==1) {
							//PMS.dbg("ClearFolderChache_in: clearing with wildcard path="+path);
							drc=db.delData(path,2); //clear MediaCache in db with wildcard
						}
						else {
							logger.warn("ClearFolderChache_in: getMediaCachePath illegal rc="+rc);
						}
					}
					if(drc==false) {
						logger.warn("ClearFolderChache_in: db.delData failed, rc="+drc);
					}
					if(child.externalProcess!=null) {
						if(PMS.rz_debug>1) logger.warn("ClearFolderChache: externalProcess remained: purge process memories, dlna="+child.getDlnaPath());
						child.externalProcess=null;
					}
					child.setResolved(false);
				}
			}
		}
	}

	public static void PurgeMemory (DLNAResource folder,int mode) {	//regzamod
		//mode==1: clear only files direct under the folder
		//mode==2: clear all files & subfolders recursively
		long tm1=System.currentTimeMillis();
		long scanid=tm1;
		String msg;
		
		PMS.isMediaCacheClearRunning=true;
		
		msg="PurgeMemory: started, folder="+folder.getName()+", mode="+mode;
		logger.debug(msg);
		logger.debug("  mode==1: PurgeMemory for only files direct under the folder");
		logger.debug("  mode==2: PurgeMemory for all files & subfolders recursively");
		PMS.get().getFrame().setStatusLine(msg);

		//FileUtil.ClearSubtitlesCache();
		PurgeMemory_in(folder,mode,scanid,0);
		
		PMS.isMediaCacheClearRunning=false;
		
		long tm2=System.currentTimeMillis();
		msg="PurgeMemory: end, Folder="+folder.getName()+", elapsed time(sec)="+((tm2-tm1)/1000.0);
		logger.debug(msg);
		PMS.get().getFrame().setStatusLine(msg);

	}
	private static synchronized void PurgeMemory_in (DLNAResource resource
		,int mode,long scanid, int layer) {
		if(layer>15) {
			logger.warn("purgeMemory_in: reached too deep layer="+layer+", return to upper");
			return;
		}
		if(true) {
			//String path=null;
			//int[] rc=new int[1];
			
			if(resource.scanid==scanid) {
				//already scaned? 
				logger.warn("PurgeMemory_in: found already scaned object="+resource);
				logger.warn("PurgeMemory_in: get back, to avoid infinite loop");
				return;
			}
			resource.scanid=scanid;
			//resource.setResolved(false);
			
			for (DLNAResource child : resource.getChildren()) {	//regzamod
				//if (child.allowScan()) {  //folder of class MapFile
				if (child.isFolder()) {  //all folders
					String trace = null;
					if (child.isFolder()) {
						trace = "PurgeMemory: Target Folder=" + child.getDlnaPath(); 
					}
					if (trace != null) {
						logger.debug(trace);
						PMS.get().getFrame().setStatusLine(trace); //slow!!
					}
					int count = child.getChildren().size();
					if (count == 0) {
						continue;
					}
					if(mode==2) { // exec subfolders recursively
						PurgeMemory_in(child,mode,scanid,layer+1);
					}
				}
				else { //may be file
					if(child.externalProcess!=null) {
						if(PMS.rz_debug>0) logger.warn("PurgeMemory: externalProcess remained: purge process memories, dlna="+child.getDlnaPath());
						child.externalProcess=null;
					}
				}
			}
		}
	}
	
	private List<RealFile> getConfiguredFolders() {
		List<RealFile> res = new ArrayList<RealFile>();
		File[] files = PMS.get().getFoldersConf();
		if (files == null || files.length == 0) {
			files = File.listRoots();
		}
		for (File f : files) {
			res.add(new RealFile(f));
		}
		return res;
	}

	private List<DLNAResource> getVirtualFolders() {
		List<DLNAResource> res = new ArrayList<DLNAResource>();
		List<MapFileConfiguration> mapFileConfs = MapFileConfiguration.parse(configuration.getVirtualFolders());
		if (mapFileConfs != null)
			for (MapFileConfiguration f : mapFileConfs) {
				res.add(new MapFile(f));
			}
		return res;
	}

	//private void addWebFolder(File webConf) {
	private DLNAResource addWebFolder(File webConf,DLNAResource pa) {
		if(PMS.rz_debug>1) PMS.dbg("addWebFolder: Start, pa sess="+pa.getCurrentSession());

		DLNAResource Root=null;
		String common_opts=null;
		if (webConf.exists()) {
			try {
				LineNumberReader br = new LineNumberReader(new InputStreamReader(new FileInputStream(webConf), "UTF-8"));
				String line = null;
				while ((line = br.readLine()) != null) {
					line = line.trim();
					if (line.length() > 0 && !line.startsWith("#") && line.indexOf("=") > -1) {
						String key = line.substring(0, line.indexOf("="));
						String value = line.substring(line.indexOf("=") + 1);
						String keys[] = parseFeedKey(key);
						try {
							if(keys[0].equals("imagefeed")
									|| keys[0].equals("audiofeed")
									|| keys[0].equals("videofeed")
									|| keys[0].equals("audiostream")
									|| keys[0].equals("videostream")) {
								String values[] = parseFeedValue(value);
								if(PMS.rz_debug>1) {
									for(int i=0; i<values.length;i++) {
										PMS.dbg("addWebFolder: url_params["+i+"]="+values[i]);
									}
								}
								DLNAResource parent = null;
								String feedname=null;  //feed folder name
								if (keys[1] != null) {
									StringTokenizer st = new StringTokenizer(keys[1], ",");
									DLNAResource currentRoot = pa;
									int cnt=0;
									int cnt_max=st.countTokens();
									while (st.hasMoreTokens()) {
										String folder = st.nextToken().trim();
										cnt++;
										if(cnt==cnt_max && folder.endsWith("/")) {
											feedname=folder.substring(0,folder.length()-1).trim();
										}
										else {
											parent = currentRoot.searchByName(folder);
											if (parent == null) {
												parent = new VirtualFolder(folder, "");
												currentRoot.addChild(parent);
											}
											currentRoot = parent;
										}
										if(cnt==1 && parent!=null) {  //root folder for web contents
											if(Root==null) Root=(DLNAResource)parent;
										}
									}
								}
								if (parent == null) {
									parent = pa;
								}
								parent.mflags|=DLNAResource.MF_WEB_FOLDER;
								parent.mflags|=DLNAResource.MF_WEB_RESUME_FOLDER;
								
								DLNAResource child=null;
								if (keys[0].equals("imagefeed")) {
									ImagesFeed ch=new ImagesFeed(values[0],feedname);
									ch.mflags|=DLNAResource.MF_WEB_RESUME_FOLDER;
									parent.addChild(ch);
									child=(DLNAResource)ch;
								} else if (keys[0].equals("videofeed")) {
									VideosFeed ch=new VideosFeed(values[0],feedname);
									ch.setUrlParams(values,common_opts);	//regzamod
									ch.mflags|=DLNAResource.MF_WEB_RESUME_FOLDER;
									parent.addChild(ch);
									child=(DLNAResource)ch;
									//ch.createSortMenu(1);	//regzamod
									//parent.addChild(new VideosFeed(values[0]));
								} else if (keys[0].equals("audiofeed")) {
									AudiosFeed ch=new AudiosFeed(values[0],feedname);
									ch.mflags|=DLNAResource.MF_WEB_RESUME_FOLDER;
									parent.addChild(ch);
									child=(DLNAResource)ch;
								} else if (keys[0].equals("audiostream")) {
									WebAudioStream ch=new WebAudioStream(values[0], values[1], values[2]);
									parent.addChild(ch);
									
									//PMS.dbg("addWebFolder: WebAudioStream name="+ch.getName()
									//	+", sess="+ch.getCurrentSession());
									
									ch.setUrlParams(values,common_opts);	//regzamod
									child=(DLNAResource)ch;
								} else if (keys[0].equals("videostream")) {
									WebVideoStream ch=new WebVideoStream(values[0], values[1], values[2]);
									parent.addChild(ch);
									ch.setUrlParams(values,common_opts);	//regzamod
									child=(DLNAResource)ch;
								}
								if(parent==pa) {
									//child is rootfolder for webcontents
									if(Root==null) Root=(DLNAResource)child;
								}
							}
							else {
								if(key.trim().equals("common_opts")) {  //
									if(PMS.rz_debug>1) PMS.dbg("addWebFolder: ===> found common_opts, value="+value);
									common_opts=value.trim();
									if(common_opts.length()==0) common_opts=null;
								}
							}
							// catch exception here and go with parsing
						} catch (ArrayIndexOutOfBoundsException e) {
							logger.info("Error at line " + br.getLineNumber() + " of WEB.conf: " + e.getMessage());
						}
					}
				}
				br.close();
			} catch (Exception e) {
				e.printStackTrace();
				logger.info("Unexpected error in WEB.conf: " + e.getMessage());
			}
		}
		return Root;
	}
	
	/**
	 * Splits the first part of a WEB.conf spec into a pair of Strings
	 * representing the resource type and its DLNA folder.
	 * 
	 * @param spec
	 *            (String) to be split
	 * @return Array of (String) that represents the tokenized entry.
	 */
	private String[] parseFeedKey(String spec) {
		String[] pair = StringUtils.split(spec, ".", 2);

		if (pair == null || pair.length < 2) {
			pair = new String[2];
		}

		if (pair[0] == null) {
			pair[0] = "";
		}

		return pair;
	}

	/**
	 * Splits the second part of a WEB.conf spec into a triple of Strings
	 * representing the DLNA path, resource URI and optional thumbnail URI.
	 * 
	 * @param spec
	 *            (String) to be split
	 * @return Array of (String) that represents the tokenized entry.
	 */
	private String[] parseFeedValue_org(String spec) {
		StringTokenizer st = new StringTokenizer(spec, ",");
		String triple[] = new String[3];
		int i = 0;
		while (st.hasMoreTokens()) {
			triple[i++] = st.nextToken();
		}
		return triple;
	}
	
	private String[] parseFeedValue(String spec) {	//regzamod
		//String[] many=spec.split(",");
		String[] many=PMSUtil.splitx1(spec,",");  //split, except escaped char
		for(int i=0;i<many.length;i++) {
			many[i]=many[i].trim();
		}
		if(many.length<3) {
			String triple[] = new String[3];
			for(int i=0;i<many.length;i++) {
				triple[i]=many[i];
			}
			return triple;
		}
		// may return values more than 3
		return many;
	}

	/**
	 * Returns iPhoto folder. Used by manageRoot, so it is usually used as a
	 * folder at the root folder. Only works when PMS is run on MacOsX. TODO:
	 * Requirements for iPhoto.
	 */
	private DLNAResource getiPhotoFolder() {
		
		VirtualFolder res = null;
		if (Platform.isMac()) {

			Map<String, Object> iPhotoLib;
			ArrayList<?> ListofRolls;
			HashMap<?, ?> Roll;
			HashMap<?, ?> PhotoList;
			HashMap<?, ?> Photo;
			ArrayList<?> RollPhotos;

			try {
				Process prc = Runtime.getRuntime().exec("defaults read com.apple.iApps iPhotoRecentDatabases");
				BufferedReader in = new BufferedReader(new InputStreamReader(prc.getInputStream()));
				String line = null;
				if ((line = in.readLine()) != null) {
					line = in.readLine(); // we want the 2nd line
					line = line.trim(); // remove extra spaces
					line = line.substring(1, line.length() - 1); // remove quotes and spaces
				}
				in.close();
				if (line != null) {
					URI tURI = new URI(line);
					iPhotoLib = Plist.load(URLDecoder.decode(tURI.toURL().getFile(), System.getProperty("file.encoding"))); // loads the (nested) properties.
					PhotoList = (HashMap<?, ?>) iPhotoLib.get("Master Image List"); // the list of photos
					ListofRolls = (ArrayList<?>) iPhotoLib.get("List of Rolls"); // the list of events (rolls)
					res = new VirtualFolder("iPhoto Library", null);
					for (Object item : ListofRolls) {
						Roll = (HashMap<?, ?>) item;
						VirtualFolder rf = new VirtualFolder(Roll.get("RollName").toString(), null);
						RollPhotos = (ArrayList<?>) Roll.get("KeyList"); // list of photos in an event (roll)
						for (Object p : RollPhotos) {
							Photo = (HashMap<?, ?>) PhotoList.get(p);
							RealFile file = new RealFile(new File(Photo.get("ImagePath").toString()));
							rf.addChild(file);
						}
						res.addChild(rf);
					}
				} else {
					logger.info("iPhoto folder not found !?");
				}
			} catch (Exception e) {
				logger.error("Something went wrong with the iPhoto Library scan: ", e);
			}
		}
		return res;
	}
	
	/**
	 * Returns Aperture folder. Used by manageRoot, so it is usually used as a
	 * folder at the root folder. Only works when PMS is run on Mac OSX. TODO:
	 * Requirements for Aperture.
	 */
	private DLNAResource getApertureFolder() {
		VirtualFolder res = null;
		
		if (Platform.isMac()) {

			Process prc = null;
			try {
				prc = Runtime.getRuntime().exec("defaults read com.apple.iApps ApertureLibraries");
				BufferedReader in = new BufferedReader(new InputStreamReader(prc.getInputStream()));
				// Every line entry is one aperture library, we want all of them as a dlna folder. 
				String line = null;
				res = new VirtualFolder("Aperture libraries", null); 
				
				while ((line = in.readLine()) != null) {
					if (line.startsWith("(") || line.startsWith(")")) {
						continue;
					}
					line = line.trim(); // remove extra spaces
					line = line.substring(1, line.lastIndexOf("\"")); // remove quotes and spaces
					VirtualFolder apertureLibrary = createApertureDlnaLibrary(line);
					
					if (apertureLibrary != null) {
						res.addChild(apertureLibrary);
					}
				}
				in.close();
				
			} catch (Exception e) {
				logger.error("Something went wrong with the aperture library scan: ", e);
			} finally {
				// Avoid zombie processes, or open stream failures...
				if (prc!=null) {
					try {
						// the process seems to always finish, so we can wait for it.
						// if the result code is not read by parent. The process might turn into a zombie (they are real!)
						prc.waitFor();
					} catch (InterruptedException e) {
						// Can this thread be interrupted? don't think so or, and even when.. what will happen?
						logger.warn("Interrupted while waiting for stream for process" + e.getMessage());
					}
					try {
						prc.getErrorStream().close();
					} catch (Exception e) {
						logger.warn("Could not close stream for output process", e);
					}
					try {
						prc.getInputStream().close();
					} catch (Exception e) {
						logger.warn("Could not close stream for output process", e);
					}
					try {
						prc.getOutputStream().close();
					} catch (Exception e) {
						logger.warn("Could not close stream for output process", e);
					}
				}
			}
		}
		return res;
	}

	private VirtualFolder createApertureDlnaLibrary(String url) throws UnsupportedEncodingException, MalformedURLException, XmlParseException, IOException, URISyntaxException {
		VirtualFolder res = null;

		if (url != null) {
			Map<String, Object> iPhotoLib;
			// every project is a album, too
			ArrayList<?> listOfAlbums;
			HashMap<?, ?> album;
			HashMap<?, ?> photoList;

			URI tURI = new URI(url);
			iPhotoLib = Plist.load(URLDecoder.decode(tURI.toURL().getFile(), System.getProperty("file.encoding"))); // loads the (nested) properties.
			photoList = (HashMap<?, ?>) iPhotoLib.get("Master Image List"); // the list of photos
			final Object mediaPath = iPhotoLib.get("Archive Path");
			String mediaName;

			if (mediaPath != null) {
				mediaName = mediaPath.toString();

				if (mediaName != null && mediaName.lastIndexOf("/") != -1 && mediaName.lastIndexOf(".aplibrary") != -1) {
					mediaName = mediaName.substring(mediaName.lastIndexOf("/"), mediaName.lastIndexOf(".aplibrary"));
				} else {
					mediaName = "unknown library";
				}
			} else {
				mediaName = "unknown library";
			}

			logger.info("Going to parse aperture library: " + mediaName);
			res  = new VirtualFolder(mediaName, null);
			listOfAlbums = (ArrayList<?>) iPhotoLib.get("List of Albums"); // the list of events (rolls)

			for (Object item : listOfAlbums) {
				album = (HashMap<?, ?>) item;

				if (album.get("Parent") == null) {
					VirtualFolder vAlbum = createApertureAlbum(photoList, album, listOfAlbums);
					res.addChild(vAlbum);
				}
			}
		} else {
			logger.info("No Aperture library found.");
		}
		return res;
	}


	private VirtualFolder createApertureAlbum(HashMap<?, ?> photoList,
							HashMap<?, ?> album, ArrayList<?> listOfAlbums) {

		ArrayList<?> albumPhotos;
		int albumId = (Integer)album.get("AlbumId");
		VirtualFolder vAlbum = new VirtualFolder(album.get("AlbumName").toString(), null);

		for (Object item : listOfAlbums) {
			HashMap<?, ?> sub = (HashMap<?, ?>) item;

			if (sub.get("Parent") != null) {
				// recursive album creation
				int parent = (Integer)sub.get("Parent");

				if (parent == albumId) {
					VirtualFolder subAlbum = createApertureAlbum(photoList, sub, listOfAlbums);
					vAlbum.addChild(subAlbum);
				}
			}
		}

		albumPhotos = (ArrayList<?>) album.get("KeyList");

		if (albumPhotos == null) {
			return vAlbum;
		}

		boolean firstPhoto = true;

		for (Object photoKey : albumPhotos) {
			HashMap<?, ? > photo = (HashMap<?, ?>) photoList.get(photoKey);

			if (firstPhoto) {
				Object x = photoList.get("ThumbPath");

				if (x!=null) {
					vAlbum.setThumbnail(x.toString());
				}
				firstPhoto = false;
			}

			RealFile file = new RealFile(new File(photo.get("ImagePath").toString()));
			vAlbum.addChild(file);
		}
		return vAlbum;
	}


	/**
	 * Returns iTunes folder. Used by manageRoot, so it is usually used as a
	 * folder at the root folder. Only works when PMS is run on MacOsX or
	 * Windows.
	 * <p>
	 * The iTunes XML is parsed fully when this method is called, so it can take
	 * some time for larger (+1000 albums) databases. TODO: Check if only music
	 * is being added.
	 * <P>
	 * This method does not support genius playlists and does not provide a
	 * media library.
	 * 
	 * @see RootFolder#getiTunesFile(boolean)
	 */
	private rz_ItunesFolder vf_itunes;	//regzamod
	private int itparse_state;	//regzamod
	
	public DLNAResource getiTunesFolder (int mode) {
		//PMS.dbg("RootFolder.getiTunesFolder: Start, mode="+mode+", vf_itunes="+vf_itunes);
		if(vf_itunes==null) {
	    	vf_itunes = new rz_ItunesFolder("iTunes Library", 0, getDefaultRenderer());
			vf_itunes.sotype=SO_ITUNE_NOTSOLVED;
		}
		
		if(PMS.rz_debug>0) {
			PMS.dbg("RootFolder.getiTunesFolder: mode=0, parse state="+vf_itunes.sotype
				+" (SOLVED="+SO_ITUNE_SOLVED+", NOT_SOLVED="+SO_ITUNE_NOTSOLVED+")");
		}

		if(mode==0){	//create only folder
			//PMS.dbg("RootFolder.getiTunesFolder: Create only folder, parse delayed until opened");
			return vf_itunes;
		}
		else if(mode==1){	//create folder & children
			//PMS.dbg("RootFolder.getiTunesFolder: mode=1, start background parse by thraeding");
			Runnable itparse = new Runnable() {
				@Override
				public void run() {
					try {
						itparse_state=1; //running
						vf_itunes.resolve_in();
						itparse_state=2; //parse ended
					} catch (Throwable t) {
						logger.error(String.format("Failed threading getiTunesFolder"),t);
					}
				}
			};
			new Thread(itparse).start();
			return vf_itunes;
		}
		else {	//create without threading
			//PMS.dbg("RootFolder.getiTunesFolder: mode=2, start foreground parse without thraeding");
			if(vf_itunes.sotype!=SO_ITUNE_SOLVED) {
				if(itparse_state==0) {  //parser not running
					//PMS.dbg("RootFolder.getiTunesFolder: parse_tread wait timeouted");
					vf_itunes.resolve_in();
					return vf_itunes;
				}
				else if(itparse_state==1) {  //parser running
					int cnt=0;
					while(true) {
						try {
							Thread.sleep(1*1000);  //msec
						} catch (InterruptedException e) {
						}
						cnt++;
						if(itparse_state==2) {
							//PMS.dbg("RootFolder.getiTunesFolder: End, vfitues="+vf_itunes);
							return vf_itunes;
						}
						else if(cnt>10) {
							logger.warn("RootFolder.getiTunesFolder: parse_thread yet running --> try some later on");
							return vf_itunes;
						}
					}
				}
				else {
					return vf_itunes;
				}
			}
			else {
				//PMS.dbg("RootFolder.getiTunesFolder: Aleady parsed");
				return vf_itunes;
			}
		}
	}
	
	public DLNAResource getiTunesFolder() {  //origin
		return (new rz_ItunesFolder(null,1,getDefaultRenderer()));
	}

	//---------------------------------------------------
	// regzam, getiTunesFile() was moved to rz_ItunesFolder.java
	//---------------------------------------------------

	private DLNAResource AutoSearchLockBtn() {  //regzam
		int btn_type=1;  //force file_type button, otherwise auto unlocked by auto_search from terminals
		int btn_mode=1;  //toggle button
		String btn_name;
		if(rz_MenuLock_sv==1) {
			btn_name="[ DispLock Web/Resume/Setting folders ]";
		}
		else {
			btn_name="[ DispLock All folders ]";
		}
		rz_VirtualVideoActionF btn=new rz_VirtualVideoActionF(btn_name,rz_MenuLock==0?false:true,btn_mode,btn_type) {
			@Override
			public boolean enable() {
				if(rz_MenuLock==0) rz_MenuLock=rz_MenuLock_sv;
				else rz_MenuLock=0;
				if(videoSettingsRes!=null) {
					// treat for videoSettings not directly under root, i.e under subfolder
					if(rz_MenuLock==0) videoSettingsRes.mflags&=~MF_HIDE;
					else videoSettingsRes.mflags|=MF_HIDE;
				}
				if(rz_MenuLock==0) {
					logger.warn("AutoSearchLockBtn: UNLOCKED!!, rz_MenuLock="+rz_MenuLock
						+", renderer="+getDefaultRenderer());
				}
				return (rz_MenuLock==0?false:true);
			}
		};
		btn.sotype=DLNAResource.SO_LOCK_BTN;
		return (DLNAResource)btn;
	}

	public VirtualFolder getVideoSettingssFolderVf(DLNAResource pa, String name) {
		if(videoSettingsRes==null) {
			return null;
		}
		VirtualFolder vf = new VirtualFolder(name, null);
		vf.rz_linkobj=videoSettingsRes;
		return vf;
	}

	/**
	 * Returns Video Settings folder. Used by manageRoot, so it is usually used
	 * as a folder at the root folder. Child objects are created when this
	 * folder is created.
	 */
	private DLNAResource getVideoSettingssFolder() {
		//int sclock=getDefaultRenderer().getRZ_AutoSearchLock();
		//int btn_type=getDefaultRenderer().getRZ_ActionButtonType();
		//btn_type=0; //autoget from getRZ_ActionButtonType()
		
		DLNAResource res = null;
		if (!configuration.getHideVideoSettings()) {
			res = new VirtualFolder(Messages.getString("PMS.37"), null);
			res.setDefaultRenderer(getDefaultRenderer());
			VirtualFolder vfSub = new VirtualFolder(Messages.getString("PMS.8"), null);
			res.addChild(vfSub);
			//PMS.dbg("getVideoSettingssFolder: getDefaultRenderer="+getDefaultRenderer());
			//PMS.dbg("getVideoSettingssFolder: res.getDefaultRenderer="+res.getDefaultRenderer());

			res.addChild(new rz_VirtualVideoActionF(Messages.getString("PMS.3"), configuration.isMencoderNoOutOfSync(),1,btn_type) {
				@Override
				public boolean enable() {
					configuration.setMencoderNoOutOfSync(!configuration
							.isMencoderNoOutOfSync());
					return configuration.isMencoderNoOutOfSync();
				}
			});

			res.addChild(new rz_VirtualVideoActionF(Messages.getString("PMS.14"), configuration.isMencoderMuxWhenCompatible(),1,btn_type) {
				@Override
				public boolean enable() {
					configuration.setMencoderMuxWhenCompatible(!configuration.isMencoderMuxWhenCompatible());

					return configuration.isMencoderMuxWhenCompatible();
				}
			});

			res.addChild(new rz_VirtualVideoActionF("  !!-- Fix 23.976/25fps A/V Mismatch --!!", configuration.isFix25FPSAvMismatch(),1,btn_type) {
				@Override
				public boolean enable() {
					configuration.setMencoderForceFps(!configuration.isFix25FPSAvMismatch());
					configuration.setFix25FPSAvMismatch(!configuration.isFix25FPSAvMismatch());
					return configuration.isFix25FPSAvMismatch();
				}
			});

			res.addChild(new rz_VirtualVideoActionF(Messages.getString("PMS.4"), configuration.isMencoderYadif(),1,btn_type) {
				@Override
				public boolean enable() {
					configuration.setMencoderYadif(!configuration.isMencoderYadif());

					return configuration.isMencoderYadif();
				}
			});

			vfSub.addChild(new rz_VirtualVideoActionF(Messages.getString("PMS.10"), configuration.isMencoderDisableSubs(),1,btn_type) {
				@Override
				public boolean enable() {
					boolean oldValue = configuration.isMencoderDisableSubs();
					boolean newValue = !oldValue;
					configuration.setMencoderDisableSubs(newValue);
					return newValue;
				}
			});

			vfSub.addChild(new rz_VirtualVideoActionF(Messages.getString("PMS.6"), configuration.getUseSubtitles(),1,btn_type) {
				@Override
				public boolean enable() {
					boolean oldValue = configuration.getUseSubtitles();
					boolean newValue = !oldValue;
					configuration.setUseSubtitles(newValue);
					return newValue;
				}
			});

			vfSub.addChild(new rz_VirtualVideoActionF(Messages.getString("MEncoderVideo.36"), configuration.isMencoderAssDefaultStyle(),1,btn_type) {
				@Override
				public boolean enable() {
					boolean oldValue = configuration.isMencoderAssDefaultStyle();
					boolean newValue = !oldValue;
					configuration.setMencoderAssDefaultStyle(newValue);
					return newValue;
				}
			});

			res.addChild(new rz_VirtualVideoActionF(Messages.getString("PMS.7"), configuration.getSkipLoopFilterEnabled(),1,btn_type) {
				@Override
				public boolean enable() {
					configuration.setSkipLoopFilterEnabled(!configuration.getSkipLoopFilterEnabled());
					return configuration.getSkipLoopFilterEnabled();
				}
			});

			res.addChild(new rz_VirtualVideoActionF("CleanUp MediaLibrary", true,2,btn_type) {
				//CleanUp MediaLibrary(DB): delete db entries if according file/dir not exist
				@Override
				public boolean enable() {
					PMS.get().getDatabase().cleanup();
					return true;
				}
			});
			res.addChild(new rz_VirtualVideoActionF("Purge Memory", true,2,btn_type) {
				//purge unused memory,buffer etc.
				@Override
				public boolean enable() {
					PMS.get().PurgeMemory();
					return true;
				}
			});
			
			res.addChild(new rz_VirtualVideoActionF(Messages.getString("PMS.27"), true,2,btn_type) {
				//Save configuration
				@Override
				public boolean enable() {
					try {
						configuration.save();
					} catch (ConfigurationException e) {
					}
					return true;
				}
			});

			res.addChild(new rz_VirtualVideoActionF(Messages.getString("LooksFrame.12"), true,2,btn_type) {
				//Restart Server
				@Override
				public boolean enable() {
					//try {
						PMS.get().reset();
					//} catch (IOException e) {
					//}
					return true;
				}
			});
		}
		return res;
	}

	/**
	 * Returns as many folders as plugins providing root folders are loaded into
	 * memory (need to implement AdditionalFolder(s)AtRoot)
	 */
	private List<DLNAResource> getAdditionalFoldersAtRoot() {
		List<DLNAResource> res = new ArrayList<DLNAResource>();
		for (ExternalListener listener : ExternalFactory.getExternalListeners()) {
			if (listener instanceof AdditionalFolderAtRoot) {
				AdditionalFolderAtRoot afar = (AdditionalFolderAtRoot) listener;
				try {
					res.add(afar.getChild());
				} catch (Throwable t) {
					logger.error(String.format("Failed to append AdditionalFolderAtRoot with name=%s, class=%s", afar.name(), afar.getClass()), t);
				}
			} else if (listener instanceof AdditionalFoldersAtRoot) {
				java.util.Iterator<DLNAResource> folders = ((AdditionalFoldersAtRoot) listener).getChildren();
				while (folders.hasNext()) {
					DLNAResource resource = folders.next();
					try {
						res.add(resource);
					} catch (Throwable t) {
						logger.error(String.format("Failed to append AdditionalFolderAtRoots with class=%s for DLNAResource=%s", listener.getClass(), resource.getClass()), t);
					}
				}
			}
		}
		return res;
	}

	@Override
	public String toString() {
		//return "RootFolder[" + getChildren() + "]";
		StringBuilder out1 = new StringBuilder();
		StringBuilder out2 = new StringBuilder();
		int dcnt=0,tcnt=0;
		int size=getChildren().size();
		for(DLNAResource d :getChildren()) {
			tcnt++;
			if((d.mflags&MF_HIDE)==0) {
				dcnt++;
				out2.append(d.getName());
			}
			else {
				out2.append(d.getName()+"(hidden)");
			}
			if(tcnt<size) {
				out2.append(", ");
			}
		}
		
		//out1.append("RootFolder: Lock="+rz_MenuLock+", Child Total="+tcnt+", Visible="+dcnt);
		out1.append("RootFolder[Child Visible/Total="+dcnt+"/"+tcnt);
		out1.append(", Name=[ ");
		out1.append(out2);
		out1.append(" ]]");
		
		return out1.toString();
	}
	
} 
