package jp.agentec.abook.abv.bl.download;

import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import jp.agentec.abook.abv.bl.acms.client.AcmsClient;
import jp.agentec.abook.abv.bl.acms.client.json.ContentVersionsJSON;
import jp.agentec.abook.abv.bl.acms.client.json.ShopSyncWatermarkJSON;
import jp.agentec.abook.abv.bl.acms.client.parameters.ContentDownloadLogParameters;
import jp.agentec.abook.abv.bl.acms.client.parameters.FetchDateParameters;
import jp.agentec.abook.abv.bl.acms.client.parameters.ShopSyncWatermarkParameters;
import jp.agentec.abook.abv.bl.common.exception.AcmsException;
import jp.agentec.abook.abv.bl.common.ABVEnvironment;
import jp.agentec.abook.abv.bl.common.exception.ABVException;
import jp.agentec.abook.abv.bl.common.exception.ABVExceptionCode;
import jp.agentec.abook.abv.bl.common.exception.NetworkDisconnectedException;
import jp.agentec.abook.abv.bl.common.log.Logger;
import jp.agentec.abook.abv.bl.common.nw.NetworkAdapter;
import jp.agentec.abook.abv.bl.data.ABVDataCache;
import jp.agentec.abook.abv.bl.data.dao.AbstractDao;
import jp.agentec.abook.abv.bl.data.dao.AcmsDao;
import jp.agentec.abook.abv.bl.data.dao.ContentDao;
import jp.agentec.abook.abv.bl.data.dao.SppDeviceDao;
import jp.agentec.abook.abv.bl.dto.ContentDto;
import jp.agentec.abook.abv.bl.dto.EnqueteDto;
import jp.agentec.abook.abv.bl.dto.SppDeviceDto;
import jp.agentec.abook.abv.bl.logic.AbstractLogic;
import jp.agentec.abook.abv.bl.logic.CategoryLogic;
import jp.agentec.abook.abv.bl.logic.ContentLogic;
import jp.agentec.abook.abv.bl.logic.ContentReadingLogLogic;
import jp.agentec.abook.abv.bl.logic.ContractLogic;
import jp.agentec.abook.abv.bl.logic.EnqueteLogic;
import jp.agentec.abook.abv.bl.logic.GroupLogic;
import jp.agentec.abook.abv.bl.logic.MasterDataLogic;
import jp.agentec.abook.abv.bl.logic.OperationGroupMasterLogic;
import jp.agentec.abook.abv.bl.logic.OperationLogic;
import jp.agentec.abook.abv.bl.logic.PushMessageLogic;
import jp.agentec.adf.util.CollectionUtil;
import jp.agentec.adf.util.DateTimeUtil;
import jp.agentec.adf.util.StringUtil;

/**
 * 新着更新を扱うクラス。
 * <p>
 * プロファイル別のAPI対応は {@link ContentRefreshApiProfile} を参照。
 * </p>
 *
 * FIXME: DBのトランザクションができていない
 */
	public class ContentRefresher {
	private static final String TAG = "ContentRefresher";
	private static ContentRefresher instance; 
	
	private ContractLogic contractLogic = AbstractLogic.getLogic(ContractLogic.class);
	private GroupLogic groupLogic = AbstractLogic.getLogic(GroupLogic.class);
	private CategoryLogic categoryLogic = AbstractLogic.getLogic(CategoryLogic.class);
	private EnqueteLogic enqueteLogic = AbstractLogic.getLogic(EnqueteLogic.class);
	private ContentLogic contentLogic = AbstractLogic.getLogic(ContentLogic.class);
	private OperationLogic operationLogic = AbstractLogic.getLogic(OperationLogic.class);
	private PushMessageLogic pushMessageLogic = AbstractLogic.getLogic(PushMessageLogic.class);

	private ContentDao contentDao = AbstractDao.getDao(ContentDao.class);
	private SppDeviceDao sppDeviceDao = AbstractDao.getDao(SppDeviceDao.class);
	private OperationGroupMasterLogic operationGroupMasterLogic = AbstractLogic.getLogic(OperationGroupMasterLogic.class);

	private ABVDataCache cache = ABVDataCache.getInstance();
	private ContentDownloader contentDownloader = ContentDownloader.getInstance();
	private NetworkAdapter networkAdapter = ABVEnvironment.getInstance().networkAdapter;

	private ContentDownloadListener contentDownloadListener;
	private RefreshContentWorker refreshWorker = null;
	private boolean needsOperationWatermarkSync;
	private boolean needsReportWatermarkSync;
	private String pendingOperationWatermarkLastChangedAt;
	private String pendingReportWatermarkLastChangedAt;
	private ContentRefreshApiProfile refreshApiProfile = ContentRefreshApiProfile.FULL;
	
	private Map<Long, Boolean> refreshingContentMap = new ConcurrentHashMap<Long, Boolean>(); // TODO: 値は使っていないのでSetに変更する
	private boolean initializingRefreshing = false;
	private static final int refreshSleepInterval = 500;
	private static final int maxRefreshWorker = 2;

	// masterDataを取得するため登録
	private MasterDataLogic masterDataLogic = AbstractLogic.getLogic(MasterDataLogic.class);
	
	public static ContentRefresher getInstance() {
		if (instance == null) {
			synchronized (ContentRefresher.class) {
				if (instance == null) {
					instance = new ContentRefresher();
				}
			}
		}
		
		return instance;
	}
	
	private ContentRefresher() {
	}

	/**
	 * コンテンツ情報をACMSから更新します。
	 * @throws ABVException　キャッシュにユーザ情報がありません。再度ログインする必要があります。
	 * @throws Exception その他、例外です。
	 * @since 1.0.0
	 */
	public void refreshContentList(ContentDownloadListener listener) throws Exception {
		refreshContentList(listener, ContentRefreshApiProfile.FULL);
	}

	/**
	 * @param apiProfile 作業一覧／報告一覧など、呼び出し元に応じたAPIセット
	 */
	public void refreshContentList(ContentDownloadListener listener, ContentRefreshApiProfile apiProfile) throws Exception {
		if (contentDownloader.getActiveCount() > 0) { //	コンテンツのダウンロード中は、自動更新を行わない。
			contentDownloader.kickTask(); // waitingで止まらないように実行を促す
			throw new ABVException(ABVExceptionCode.C_I_CONTENT_0002);
		}

		networkAdapter = ABVEnvironment.getInstance().networkAdapter;
		if (networkAdapter != null && !networkAdapter.isNetworkConnected()) { // NWのチェック
			throw new NetworkDisconnectedException();
		}
		if (!AcmsClient.getInstance(cache.getUrlPath(), networkAdapter).checkSid(cache.getMemberInfo().sid)) { // SIDのチェックをここで行う
			throw new ABVException(ABVExceptionCode.S_E_ACMS_1403);
		}
		if (isRefreshing()) { // すでに実行中か
			throw new ABVException(ABVExceptionCode.C_I_CONTENT_0001);
		}

		refreshApiProfile = apiProfile != null ? apiProfile : ContentRefreshApiProfile.FULL;

		if (listener != null) {
			contentDownloadListener = listener;
		}

		initializingRefreshing = true;
		contentDownloader.pauseAll();
		refreshWorker = new RefreshContentWorker();
		refreshWorker.setPriority(Thread.MIN_PRIORITY);
		refreshWorker.start();
	}

	public void addRefreshingContentId(long contentId) {
		refreshingContentMap.put(contentId, false);
	}
	
	public void removeRefreshingContentId(long contentId) {
		refreshingContentMap.remove(contentId);
	}
	
	public boolean isBusyRefreshingContent() {
		return refreshingContentMap.size() > maxRefreshWorker;
	}
	
	public boolean isRefreshing() {
		return (initializingRefreshing || (refreshingContentMap.size() > 0));
	}
	
	public void setFail() {
		initializingRefreshing = false;
		refreshingContentMap.clear();
	}

	private void initializeMasterDataWithRetry() throws Exception {
		Logger.d(TAG, "before fetchDate : " + ABVDataCache.getInstance().tempMasterDataFetchDate);
		String fetchDate = null;
		for (int i = 0; i < 4; i++) {
			try {
				fetchDate = masterDataLogic.initializeMasterData(ABVDataCache.getInstance().tempMasterDataFetchDate, i);
				break;
			} catch (Exception e) {
				Logger.e(TAG, "initializeMasterData Exception", e);
				if (i == 3) {
					throw new Exception("initializeMasterData");
				} else {
					Thread.sleep(500);
				}
			}
		}
		Logger.d(TAG, "after fetchDate : " + fetchDate);
		if (fetchDate != null) {
			ABVDataCache.getInstance().tempMasterDataFetchDate = fetchDate;
		}
	}

	private boolean queryShopSyncWatermark(String screen, String storedLastFetch, boolean assignToOperationPending)
			throws NetworkDisconnectedException, AcmsException {
		ShopSyncWatermarkParameters p = new ShopSyncWatermarkParameters(cache.getMemberInfo().sid, screen,
				StringUtil.isNullOrEmpty(storedLastFetch) ? null : storedLastFetch.trim());
		ShopSyncWatermarkJSON json = AcmsClient.getInstance(cache.getUrlPath(), networkAdapter).checkShopSyncWatermark(p);
		if (assignToOperationPending) {
			pendingOperationWatermarkLastChangedAt = json.lastChangedAt;
		} else {
			pendingReportWatermarkLastChangedAt = json.lastChangedAt;
		}
		return json.needsFullSync;
	}

	private void evaluateShopSyncWatermarks() {
		needsOperationWatermarkSync = false;
		needsReportWatermarkSync = false;
		pendingOperationWatermarkLastChangedAt = null;
		pendingReportWatermarkLastChangedAt = null;
		try {
			AcmsDao dao = AbstractDao.getDao(AcmsDao.class);
			switch (refreshApiProfile) {
				case FULL:
					needsOperationWatermarkSync = queryShopSyncWatermark(
							ShopSyncWatermarkParameters.SCREEN_OPERATION_LIST,
							dao.selectShopSyncWatermarkOperationListLastFetchDate(), true);
					needsReportWatermarkSync = queryShopSyncWatermark(
							ShopSyncWatermarkParameters.SCREEN_REPORT_LIST,
							dao.selectShopSyncWatermarkReportListLastFetchDate(), false);
					break;
				case OPERATION_LIST_SCREEN:
					needsOperationWatermarkSync = queryShopSyncWatermark(
							ShopSyncWatermarkParameters.SCREEN_OPERATION_LIST,
							dao.selectShopSyncWatermarkOperationListLastFetchDate(), true);
					break;
				case REPORT_LIST_SCREEN:
					needsReportWatermarkSync = queryShopSyncWatermark(
							ShopSyncWatermarkParameters.SCREEN_REPORT_LIST,
							dao.selectShopSyncWatermarkReportListLastFetchDate(), false);
					break;
			}
		} catch (Exception e) {
			Logger.w(TAG, "shopSyncWatermark check failed. run fail-safe sync.", e);
			switch (refreshApiProfile) {
				case FULL:
					needsOperationWatermarkSync = true;
					needsReportWatermarkSync = true;
					break;
				case OPERATION_LIST_SCREEN:
					needsOperationWatermarkSync = true;
					break;
				case REPORT_LIST_SCREEN:
					needsReportWatermarkSync = true;
					break;
			}
		}
	}

	private void saveShopSyncWatermarkOperationListAfterRefresh(boolean withLatestRefetch) {
		String lastChangedAt = pendingOperationWatermarkLastChangedAt;
		try {
			if (withLatestRefetch) {
				ShopSyncWatermarkParameters latestParam = new ShopSyncWatermarkParameters(cache.getMemberInfo().sid,
						ShopSyncWatermarkParameters.SCREEN_OPERATION_LIST, null);
				ShopSyncWatermarkJSON latestJson = AcmsClient.getInstance(cache.getUrlPath(), networkAdapter)
						.checkShopSyncWatermark(latestParam);
				if (latestJson != null && latestJson.lastChangedAt != null && latestJson.lastChangedAt.length() > 0) {
					lastChangedAt = latestJson.lastChangedAt;
				}
			}
			if (lastChangedAt == null || lastChangedAt.length() == 0) {
				return;
			}
			AbstractDao.getDao(AcmsDao.class).updateShopSyncWatermarkOperationListLastFetchDate(lastChangedAt);
		} catch (Exception e) {
			Logger.w(TAG, "failed to save shopSyncWatermark OPERATION_LIST lastFetchDate.", e);
		}
	}

	private void saveShopSyncWatermarkReportListAfterRefresh(boolean withLatestRefetch) {
		String lastChangedAt = pendingReportWatermarkLastChangedAt;
		try {
			if (withLatestRefetch) {
				ShopSyncWatermarkParameters latestParam = new ShopSyncWatermarkParameters(cache.getMemberInfo().sid,
						ShopSyncWatermarkParameters.SCREEN_REPORT_LIST, null);
				ShopSyncWatermarkJSON latestJson = AcmsClient.getInstance(cache.getUrlPath(), networkAdapter)
						.checkShopSyncWatermark(latestParam);
				if (latestJson != null && latestJson.lastChangedAt != null && latestJson.lastChangedAt.length() > 0) {
					lastChangedAt = latestJson.lastChangedAt;
				}
			}
			if (lastChangedAt == null || lastChangedAt.length() == 0) {
				return;
			}
			AbstractDao.getDao(AcmsDao.class).updateShopSyncWatermarkReportListLastFetchDate(lastChangedAt);
		} catch (Exception e) {
			Logger.w(TAG, "failed to save shopSyncWatermark REPORT_LIST lastFetchDate.", e);
		}
	}

	private class RefreshContentWorker extends Thread {
		private boolean interrupt = false;
		
		public void stopWorker() {
			interrupt = true;
		}
		
		@Override
		public void run() {
			Logger.i(TAG, "refresh main thread start.-----------------------------------------------");
			
			interrupt = false;
			try {
                boolean isFinishedContentCheck = false;
				boolean ranContentVersionPhase = false;
				List<ContentDto> localContents = contentDao.getAllContents();
				if (networkAdapter.isNetworkConnected()) { // オンライン時の処理
					// 共通: アンケート再送・DLログ・閲覧ログ送信
					resendLog();

					evaluateShopSyncWatermarks();
					boolean needsAnyDbSync = needsOperationWatermarkSync || needsReportWatermarkSync;
					boolean ranOperationPhase = false;
					boolean ranReportPhase = false;

					if (needsAnyDbSync) {
						ContentRefreshApiProfile profile = refreshApiProfile;
						Logger.d(TAG, "shopSyncWatermark needs sync op=%s report=%s apiProfile=%s",
								needsOperationWatermarkSync, needsReportWatermarkSync, profile);
						switch (profile) {
							case FULL:
								if (needsOperationWatermarkSync && needsReportWatermarkSync) {
									// 両方更新時は従来 FULL と同じ依存順で一括実行
									contractLogic.initializeContractServiceOption();
									groupLogic.initializeGroups();
									categoryLogic.initializeCategories();
									initializeMasterDataWithRetry();
									operationGroupMasterLogic.setOperationGroupMaster();
									ranOperationPhase = true;
									ranReportPhase = true;
								} else if (needsOperationWatermarkSync) {
									contractLogic.initializeContractServiceOption();
									operationLogic.syncWorkingGroupsFromServer();
									categoryLogic.initializeCategories();
									operationGroupMasterLogic.setOperationGroupMaster();
									ranOperationPhase = true;
								} else if (needsReportWatermarkSync) {
									groupLogic.initializeGroups();
									initializeMasterDataWithRetry();
									ranReportPhase = true;
								}
								break;
							case OPERATION_LIST_SCREEN:
								if (needsOperationWatermarkSync) {
									contractLogic.initializeContractServiceOption();
									operationLogic.syncWorkingGroupsFromServer();
									categoryLogic.initializeCategories();
									operationGroupMasterLogic.setOperationGroupMaster();
									ranOperationPhase = true;
								}
								break;
							case REPORT_LIST_SCREEN:
								if (needsReportWatermarkSync) {
									groupLogic.initializeGroups();
									initializeMasterDataWithRetry();
									ranReportPhase = true;
								}
								break;
						}
					} else {
						Logger.d(TAG, "skip full DB sync by shopSyncWatermark.");
					}

					if (interrupt) { // この時点で停止要求が来た場合先には進まない。（ServiceOption/Group/Categoryの更新は１セットで行う（トランザクションはそれぞれ別））
						Logger.d(TAG, "stop refresh worker before content update.");
						setFail();
						updateRefreshContentListState(-1L, null);
						return;
					}
					// contentVersion：FULL は報告ウォーターマーク時のみ（従来）。作業一覧／報告一覧プロファイルは画面ごとに必ず実行し、
					// t_content が無いせいで operationList の新規行が落ちる問題と、報告側だけ最新化されない問題を避ける。
					boolean runContentVersion =
							(needsReportWatermarkSync && refreshApiProfile == ContentRefreshApiProfile.FULL)
							|| refreshApiProfile == ContentRefreshApiProfile.OPERATION_LIST_SCREEN
							|| refreshApiProfile == ContentRefreshApiProfile.REPORT_LIST_SCREEN;
					if (runContentVersion) {
						Logger.d(TAG, "contentVersion phase apiProfile=%s needsReportWm=%s", refreshApiProfile,
								needsReportWatermarkSync);
						isFinishedContentCheck = retrieveServerContent(localContents); // contentVersion（DL は非同期）
						ranContentVersionPhase = true;
					}
					if (ranOperationPhase) {
						saveShopSyncWatermarkOperationListAfterRefresh(ranContentVersionPhase);
					}
					if (ranReportPhase) {
						saveShopSyncWatermarkReportListAfterRefresh(ranContentVersionPhase);
					}
				}
				
				deleteLocalContent(localContents, isFinishedContentCheck); // コンテンツ削除処理
				initializingRefreshing = false;

				if (!isRefreshing()) {
					updateRefreshContentListState(-1L, null);
				}

			} catch (Exception e) {
				Logger.e("refreshContent failed.", e);
				updateRefreshContentListState(-1L, e);
			}
			contentDownloader.kickTask();
			Logger.i(TAG, "refresh main thread end.-----------------------------------------------");
		}

        /**
         * ローカルコンテンツの公開期限や権限喪失のチェックを行う
         * @param localContents List<ContentDto> 新着や更新対象のチェックが終わったコンテンツリスト
         * @param isFinishedContentCheck boolean localContentsとServerコンテンツの比較が終わったか否か
         *                               falseは変更なし(304)の場合なので、公開期限切れのチェックのみを行う
         * @return boolean
         */
		private boolean deleteLocalContent(List<ContentDto> localContents, boolean isFinishedContentCheck) {
			if (CollectionUtil.isEmpty(localContents)) {
				return false;
			}

			boolean isOnline = networkAdapter.isNetworkConnected();
			//	公開期間が過ぎたコンテンツを削除するため、サーバ時間を格納する変数。オフラインの場合、ローカルタイムを基準にしてコンテンツを削除するために、ローカルタイムで初期化する。
            Date serverTime = ABVEnvironment.getInstance().getCurrentTime(isOnline);

			boolean hasDeleted = false;
			for (ContentDto localDto : localContents) {
				if (interrupt) {
					Logger.d(TAG, "stop refresh worker before doing with local content [%s]", localDto.contentName);
					break;
				}

				// 権限喪失,非公開,削除時のDL済コンテンツ強制削除 Option
				if (isOnline && cache.serviceOption.isNoAuthDelete() && isFinishedContentCheck) { // 権限喪失削除：　onで、公開期限削除がoffのときこれだと問題（公開一覧には含まれなくなる！？）
					hasDeleted = true;
                    Logger.d(TAG, "[deleteLocalContent]:Delete For NoAuth Content");
                    contentLogic.deleteContent(localDto, true);
				} else {
                    // 公開終了時のDL済コンテンツ強制削除 Option
					if (cache.serviceOption.isUndelivarableDelete()) {
						//	公開終了コンテンツ削除がtrueの場合、公開期間をチェックして、期間が過ぎたら削除
						if (localDto.deliveryEndDate != null && serverTime.getTime() >= localDto.deliveryEndDate.getTime()) {
							hasDeleted = true;
                            Logger.d(TAG, "[deleteLocalContent]:Id=" + localDto.contentId + ":deliveryEndDate:" + localDto.deliveryEndDate);
                            contentLogic.deleteContent(localDto, true);
						}

						//	cache.serviceOption.getUndelivarableDelete()がtrue、localDto.getDeliveryEndDateがnullの場合
						//	コンテンツはダウンロード済みだとする。
						//	実際にはサーバでこのコンテンツの終了期間を設定したとしても、サーバからのコンテンツ一覧にこのコンテンツがでないため、削除しようがない。
						//  ⇒仕様
					}

					//	権限喪失時削除オプションがfalse
					//	公開期間が過ぎてないのに、サーバからのコンテンツリストに存在しないコンテンツ、つまり権限喪失コンテンツである
					//	権限喪失時削除オプションがfalseなので、ダウンロードしていないコンテンツのみ削除
					if (isOnline && !localDto.downloadedFlg && isFinishedContentCheck) {
						hasDeleted = true;
                        Logger.d(TAG, "[deleteContent]:権限喪失:contentId=" + localDto.contentId);
                        contentLogic.deleteContent(localDto, true);
					}
				}
				
				if (hasDeleted) {
					ContentDownloader.getInstance().cancel(localDto.contentId);
				}
				
			}
			return hasDeleted;
		}

        /**
         * API「contentVersion」から取得したサーバ公開のコンテンツリストをローカルコンテンツを比較して、
         * 新規や更新の場合はダウンロードを行う
         * また、変更日付（FetchDate）による更新有無を判断して３０４の場合は処理を行わないようにする
         * ただし、３０４の場合でもサービスオプション設定に従って公開期限切れのチェックを行う
         * @param localContents List<ContentDto> ローカルに保存されたコンテンツのリスト
         * @return 変更あり:true、変更なし：false
         * @throws Exception
         */
		private boolean retrieveServerContent(List<ContentDto> localContents) throws Exception {
            // コンテンツバージョン用lastFetchDateの取得
            AcmsDao dao = AbstractDao.getDao(AcmsDao.class);
            String lastFetchDate = dao.selectContentVersionLastFetchDate();

            FetchDateParameters param = new FetchDateParameters(cache.getMemberInfo().sid, lastFetchDate);
			ContentVersionsJSON json = AcmsClient.getInstance(cache.getUrlPath(), networkAdapter).contentVersion(param);
            if (json == null) {
                // 変更なし(304)の場合、FetchDateを更新する必要がないため、tempはクリアする
                ABVEnvironment.getInstance().tempContentVersionLastFetchDate = "";
                return false;
            }

			// リソースパターンの値を取得する
			ABVEnvironment.getInstance().resourcePatternType = json.resourcePatternType;

            // 一時値を保存。ダウンロード処理が完了したあとにlastFetchDateを更新
            String fetchDate = json.fetchDate;
            ABVEnvironment.getInstance().tempContentVersionLastFetchDate = fetchDate;

            // sppDeviceDtoを登録
            List<SppDeviceDto> sppDeviceDtoList = json.sppDeviceDtoList;
            if (CollectionUtil.isNotEmpty(sppDeviceDtoList)) {
            	// ローカルに保存されてるIDリストを取得（登録・更新の判定のため）
				List<Integer> localSppDeviceIdList = sppDeviceDao.getSppDeviceIdList();
				for (SppDeviceDto sppDeviceDto : sppDeviceDtoList) {
					if (CollectionUtil.isNotEmpty(localSppDeviceIdList) && localSppDeviceIdList.contains(sppDeviceDto.sppDeviceId)) {
						SppDeviceDto localSppDevice = sppDeviceDao.getSppDeviceById(sppDeviceDto.sppDeviceId);
						// ペアリング情報だけローカルから取得して、更新する
						sppDeviceDto.pairingDeviceName = localSppDevice.pairingDeviceName;
						sppDeviceDto.pairingDeviceAddress = localSppDevice.pairingDeviceAddress;
						sppDeviceDao.updateSppDevice(sppDeviceDto);

						// 削除のため、更新したら該当するIDを削除する
						localSppDeviceIdList.remove(localSppDeviceIdList.indexOf(sppDeviceDto.sppDeviceId));
					} else {
						// 取得したSPP通信端末のデータベースへ登録
						sppDeviceDao.insert(sppDeviceDto);
					}
				}

				// ローカル情報削除（サーバーと端末データの差分）
				if (CollectionUtil.isNotEmpty(localSppDeviceIdList)) {
					for (Integer deleteSppDeviceId : localSppDeviceIdList) {
						sppDeviceDao.deleteById(deleteSppDeviceId);
					}
				}
			}

            List<ContentDto> serverContents = json.contentVersions;
			// DTO Info:contentId, metaVersion, resourceVersion, contentNameKana, readerShareFlg

			for (ContentDto serverContentDto : serverContents) {
				while (isBusyRefreshingContent()) {
					if (interrupt) {
						Logger.d(TAG, "stop refresh worker while waiting to do with content [%s]", serverContentDto.contentName);
						break;
					}
					Logger.d(TAG, "refreshWorker is busy.");
					Thread.sleep(refreshSleepInterval);								
				}

				if (interrupt) {
					Logger.d(TAG, "stop refresh worker before doing with content [%s]", serverContentDto.contentName);
					break;
				}

				if (localContents != null) {
					int localContentIndex = localContents.indexOf(serverContentDto);
					if (localContentIndex >= 0) {
						ContentDto localDto = localContents.get(localContentIndex);
						if (!serverContentDto.equalsVersion(localDto)) { //	更新 ContentInfo.zipをダウンロードする。
							contentDownloader.downloadContentInfo(serverContentDto.contentId);
							if (serverContentDto.isLinkType() && localDto.isDownloadable(true)) {
								contentDownloader.addAutoDownload(serverContentDto.contentId); 
							}
						}
						else if (!localDto.downloadedFlg && serverContentDto.isLinkType() && localDto.isDownloadable(true)) { // ダウンロード未完了も追加
							contentDownloader.addAutoDownload(serverContentDto.contentId); 
						}

						localContents.remove(localDto); //	既に存在するコンテンツはローカルのリストからはずしておく。
					} else {
						//	新規 ContentInfo.zipをダウンロードする。
						contentDownloader.downloadContentInfo(serverContentDto.contentId);
						if (serverContentDto.isLinkType() && serverContentDto.isDownloadable(true)) {
							contentDownloader.addAutoDownload(serverContentDto.contentId); 
						}
					}
				}
			}
            return true;
        }

	}
	
	public void updateRefreshContentListState(long contentId, Exception e) {
		Logger.d(TAG, "[updateRefreshContentListState]: contentId=%s", contentId);
		//noinspection VariableNotUsedInsideIf
		if (e != null) {
			setFail();
		}

		if (contentDownloadListener != null) {
			if (e == null) {
				try {
					switch (refreshApiProfile) {
						case FULL:
							// コンテンツ ZIP 並行 DL 中は refreshingContentMap が残り得る。一覧反映は最終完了まで遅延。
							if (!isRefreshing()) {
								operationLogic.initializeOperations();
								pushMessageLogic.initializePushMessage();
							}
							break;
						case OPERATION_LIST_SCREEN:
						case REPORT_LIST_SCREEN:
							// ワーカー完了コールバックは contentId=-1。並行コンテンツ DL で isRefreshing が true のときも
							// operationList を保存できない問題を避ける。REPORT 側は ZIP 完了ごとのコールバックが多いので
							// contentId>=0 かつ refreshing 中はスキップし、最終完了時のみ同期する。
							if (!isRefreshing() || contentId < 0) {
								operationLogic.syncOperationListFromServer();
							}
							if (!isRefreshing()) {
								pushMessageLogic.initializePushMessage();
							}
							break;
					}
				} catch (Exception e1) {
					Logger.e(TAG, e1);
					e = e1;
				}
			}
			contentDownloadListener.onRefreshedContent(e == null, contentId, e);
		}
	}
	
	public void stopRefresh() {
		if (isRefreshing() && refreshWorker != null) {
			refreshWorker.stopWorker();
		}
	}
	
	/**
	 * ログ等を送信する
	 */
	private void resendLog() {
		// 失敗したアンケート結果を再送する
		resendEnquete();
		// 配信ログを再送する
		batchSendDownloadLog();
		// 閲覧ログを再送する
		AbstractLogic.getLogic(ContentReadingLogLogic.class).batchSendReadingLog();
	}

	/**
	 * アンケート回答を送信する
	 */
	private void resendEnquete() {
		List<EnqueteDto> dtoList = enqueteLogic.getReplyEnquete();

		if (dtoList != null && dtoList.size() > 0) {
			for (EnqueteDto dto : dtoList) {
				long contentId = dto.contentId;
				long abObjectId = dto.abObjectId;
				Date ts = dto.replyDateStr;
				String param = dto.param;
				boolean result = false;
				try {
					result = enqueteLogic.enqueteReply(contentId, abObjectId, ts, param);
				} catch (Exception e) {
					Logger.e(TAG, "enqueteReply failed.", e); // ignore
				}

				if (result) {
					// 再送成功したアンケートを回答済みとしてアップデートする
					dto.replyFlg = true;
					boolean upResult = enqueteLogic.updateEnquete(dto);
					Logger.d(TAG, "アップデート結果 : %s", upResult);
				}
			}
		}
	}

	/**
	 * ダウンロードログを送信する
	 */
	public void batchSendDownloadLog() {
		if (ABVEnvironment.getInstance().disableLogSend) {
			return;
		}
		try {
			List<ContentDto> list = contentDao.getSendLog();
			AcmsClient acms = AcmsClient.getInstance(cache.getUrlPath(), ABVEnvironment.getInstance().networkAdapter);
			
			if (list != null && list.size() > 0) {
				for (ContentDto dto : list) { // FIXME: Readerがここに来るかどうか
					ContentDownloadLogParameters downloadParam = new ContentDownloadLogParameters(cache.getMemberInfo().sid
							, dto.contentId, DateTimeUtil.dateToTimestamp(dto.downloadStartDate)
							, dto.resourceVersion, ABVEnvironment.DeviceTypeId, dto.getDownloadStatus());
					downloadParam.setDownloadSize(dto.downloadedBytes);
					downloadParam.setDownloadEndtime(DateTimeUtil.dateToTimestamp(dto.downloadEndDate));
					
					if (acms.contentDownloadLog(downloadParam)) {
						dto.logSendedFlg = true;
						contentDao.updateLogSendFlg(dto);
					}
				}
			}
		} catch (Exception e) {
			Logger.e("batchSendDownloadLog failed.", e); // 例外は上にあげない。失敗したら次送られるはず（要確認）
		}
	}
}
