/**
 * DrezSync booking field.
 *
 * Renders the booking field's date and time picker, loads the open times for
 * the chosen date from the server, and keeps the selection in a hidden input
 * for submission. After a failed submit the field reloads its list of times, so
 * nothing has to be re-entered. Styles are inline so they render the same
 * regardless of theme or CSS caching.
 */
( function( $ ) {
	$( function() {
		if ( typeof drezsyncBooking === 'undefined' ) {
			return;
		}

		var pillBase = 'appearance:none;-webkit-appearance:none;border:1px solid #d5d5d5;background:#fff;color:#434343;padding:8px 15px;border-radius:999px;font-size:14px;line-height:1.2;cursor:pointer;';

		// Every booking field on the form registers here so they can see each
		// other's current selection and re-mark when anything changes.
		var registry = [];

		function ymd( d ) {
			return d.getFullYear() + '-' +
				String( d.getMonth() + 1 ).padStart( 2, '0' ) + '-' +
				String( d.getDate() ).padStart( 2, '0' );
		}

		var min = ymd( new Date() );

		function addDays( dateStr, n ) {
			var p = dateStr.split( '-' );
			var d = new Date( parseInt( p[0], 10 ), parseInt( p[1], 10 ) - 1, parseInt( p[2], 10 ) );
			d.setDate( d.getDate() + n );
			return ymd( d );
		}

		function pretty( t ) {
			var parts = t.split( ':' );
			var h     = parseInt( parts[0], 10 );
			var ampm  = h >= 12 ? 'PM' : 'AM';
			var h12   = h % 12;
			if ( 0 === h12 ) {
				h12 = 12;
			}
			return h12 + ':' + parts[1] + ' ' + ampm;
		}

		function fieldIdFrom( $value ) {
			var name    = $value.attr( 'name' ) || '';
			var matched = name.match( /input_(\d+)/ );
			return matched ? matched[1] : 0;
		}

		// Seconds since epoch for a date and time, in the browser's local clock.
		// Only used to compare intervals against each other, so the zone cancels out.
		function ts( dateStr, timeStr ) {
			var d = dateStr.split( '-' );
			var t = timeStr.split( ':' );
			return new Date( parseInt( d[0], 10 ), parseInt( d[1], 10 ) - 1, parseInt( d[2], 10 ), parseInt( t[0], 10 ), parseInt( t[1], 10 ), 0 ).getTime() / 1000;
		}


		// Ask every field to re-mark itself after any change.
		function recompute() {
			$.each( registry, function( i, f ) {
				f.refresh();
			} );
		}

		function pad( n ) {
			return String( n ).padStart( 2, '0' );
		}

		function tint( hex, a ) {
			var m = /^#?([0-9a-f]{6})$/i.exec( hex || '' );
			if ( ! m ) {
				return '#f2efe9';
			}
			var n = parseInt( m[1], 16 );
			return 'rgba(' + ( ( n >> 16 ) & 255 ) + ',' + ( ( n >> 8 ) & 255 ) + ',' + ( n & 255 ) + ',' + ( a || 0.12 ) + ')';
		}

		// A month-grid date picker. It asks the server which days in the visible
		// month are available and greys the rest, so a visitor sees at a glance
		// where they can book instead of clicking blindly. It drives a hidden input
		// (via cfg.onPick) so all the existing booking logic is unchanged.
		function makeCalendar( $el, cfg ) {
			var store = cfg.store || { open: {}, fetched: {} };
			var view  = new Date();
			view.setDate( 1 );

			function key( y, m ) {
				return y + '-' + m;
			}

			function ensure( y, m, cb ) {
				if ( store.fetched[ key( y, m ) ] ) {
					cb( true );
					return;
				}
				var data = {
					action:  'drezsync_month',
					nonce:   drezsyncBooking.nonce,
					formId:  drezsyncBooking.formId,
					fieldId: cfg.fieldId,
					year:    y,
					month:   m,
					mode:    cfg.mode
				};
				if ( cfg.sourceValFn ) {
					var sv = cfg.sourceValFn();
					if ( ! sv ) {
						if ( cfg.onCal ) { cfg.onCal( '' ); }
						cb( false );
						return;
					}
					data.calValue = sv;
				}
				$.post( drezsyncBooking.ajaxUrl, data ).done( function( res ) {
					var days = ( res && res.success && res.data.days ) ? res.data.days : [];
					for ( var i = 0; i < days.length; i++ ) {
						store.open[ y + '-' + pad( m ) + '-' + pad( days[ i ] ) ] = true;
					}
					if ( cfg.onCal ) { cfg.onCal( ( res && res.data && res.data.cal ) ? res.data.cal : '' ); }
					store.fetched[ key( y, m ) ] = true;
					cb( true );
				} ).fail( function() {
					// Do NOT cache a failed load. Leaving the month unfetched lets a
					// later visit retry it, instead of one failed request greying the
					// month out for the rest of the session.
					cb( true );
				} );
			}

			function navStyle( on ) {
				return 'appearance:none;border:none;background:none;font-size:20px;line-height:1;padding:2px 8px;color:' + ( on ? '#434343' : '#ccc' ) + ';cursor:' + ( on ? 'pointer' : 'default' ) + ';';
			}

			function dayStyle( ok, hl ) {
				var base = 'appearance:none;border:1px solid transparent;border-radius:8px;padding:7px 0;font-size:13px;line-height:1;text-align:center;width:100%;';
				if ( 'edge' === hl ) {
					return base + 'background:' + cfg.color + ';border-color:' + cfg.color + ';color:#fff;cursor:pointer;';
				}
				if ( 'mid' === hl ) {
					return base + 'background:' + tint( cfg.color, 0.22 ) + ';color:#434343;cursor:pointer;';
				}
				if ( ok ) {
					return base + 'background:#fff;color:#434343;cursor:pointer;';
				}
				return base + 'background:transparent;color:#c8c8c8;cursor:not-allowed;text-decoration:line-through;';
			}

			function paint( y, m ) {
				var nowY   = new Date().getFullYear();
				var nowM   = new Date().getMonth() + 1;
				var prevOK = ! ( y === nowY && m === nowM );

				var h = '<div style="max-width:280px;">';
				h += '<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;">';
				h += '<button type="button" class="dz-cal__prev" aria-label="' + drezsyncBooking.prevMonth + '" style="' + navStyle( prevOK ) + '"' + ( prevOK ? '' : ' disabled="disabled"' ) + '>‹</button>';
				h += '<span style="font-size:14px;font-weight:600;color:#434343;">' + drezsyncBooking.months[ m - 1 ] + ' ' + y + '</span>';
				h += '<button type="button" class="dz-cal__next" aria-label="' + drezsyncBooking.nextMonth + '" style="' + navStyle( true ) + '">›</button>';
				h += '</div>';
				h += '<div style="display:grid;grid-template-columns:repeat(7,1fr);gap:2px;margin-bottom:2px;">';
				for ( var w = 0; w < 7; w++ ) {
					h += '<span style="text-align:center;font-size:11px;color:#999;">' + drezsyncBooking.weekdays[ w ] + '</span>';
				}
				h += '</div><div style="display:grid;grid-template-columns:repeat(7,1fr);gap:2px;">';

				var lead = new Date( y, m - 1, 1 ).getDay();
				var dim  = new Date( y, m, 0 ).getDate();
				for ( var i = 0; i < lead; i++ ) {
					h += '<span></span>';
				}
				for ( var d = 1; d <= dim; d++ ) {
					var ymd = y + '-' + pad( m ) + '-' + pad( d );
					var ok  = cfg.selectable( ymd, store );
					var hl  = cfg.highlight ? cfg.highlight( ymd ) : ( ( cfg.selectedYmd && cfg.selectedYmd() === ymd ) ? 'edge' : '' );
					var live = ok || 'edge' === hl || 'mid' === hl;
					h += '<button type="button" class="dz-cal__day" data-ymd="' + ymd + '"' + ( live ? '' : ' data-off="1"' ) + ( hl ? ' data-hl="' + hl + '"' : '' ) + ' style="' + dayStyle( ok, hl ) + '">' + d + '</button>';
				}
				h += '</div></div>';
				$el.html( h );
			}

			function draw() {
				var y = view.getFullYear();
				var m = view.getMonth() + 1;
				$el.html( '<p style="margin:0;font-size:13px;color:#999;">' + drezsyncBooking.calLoading + '</p>' );
				ensure( y, m, function( ok ) {
					if ( ! ok ) {
						$el.html( '<p style="margin:0;font-size:13px;color:#999;">' + drezsyncBooking.pickSource + '</p>' );
						return;
					}
					paint( y, m );
				} );
			}

			$el.on( 'click', '.dz-cal__prev', function() {
				if ( $( this ).is( '[disabled]' ) ) {
					return;
				}
				view.setMonth( view.getMonth() - 1 );
				draw();
			} );
			$el.on( 'click', '.dz-cal__next', function() {
				view.setMonth( view.getMonth() + 1 );
				draw();
			} );
			$el.on( 'click', '.dz-cal__day', function() {
				var $b = $( this );
				if ( $b.attr( 'data-off' ) ) {
					return;
				}
				cfg.onPick( $b.attr( 'data-ymd' ) );
			} );
			$el.on( 'mouseenter', '.dz-cal__day', function() {
				var $b = $( this );
				if ( $b.attr( 'data-off' ) || $b.attr( 'data-hl' ) ) {
					return;
				}
				$b.css( 'background', tint( cfg.color ) );
			} );
			$el.on( 'mouseleave', '.dz-cal__day', function() {
				var $b = $( this );
				if ( $b.attr( 'data-off' ) || $b.attr( 'data-hl' ) ) {
					return;
				}
				$b.css( 'background', '#fff' );
			} );

			return {
				draw: draw,
				reset: function() {
					store.open = {};
					store.fetched = {};
				},
				gotoMonth: function( ymd ) {
					var p = ( ymd || '' ).split( '-' );
					if ( 3 === p.length ) {
						view = new Date( parseInt( p[0], 10 ), parseInt( p[1], 10 ) - 1, 1 );
					}
					draw();
				},
				repaint: function() {
					var y = view.getFullYear();
					var m = view.getMonth() + 1;
					if ( store.fetched[ key( y, m ) ] ) {
						paint( y, m );
					}
				}
			};
		}

		function initSlot( $wrap ) {
			var $value  = $wrap.find( '.drezsync-booking__value' );
			var $date   = $wrap.find( '.drezsync-booking__date' );
			var $times  = $wrap.find( '.drezsync-booking__times' );
			var color   = $wrap.attr( 'data-color' ) || '#ff4e43';
			var slotLen = parseInt( $wrap.attr( 'data-slot' ), 10 ) || 30;
			var pillOn  = pillBase + 'background:' + color + ';border-color:' + color + ';color:#fff;';
			var pillErr = pillBase + 'background:#b32d23;border-color:#b32d23;color:#fff;';
			var pillDim = pillBase + 'opacity:.4;cursor:not-allowed;text-decoration:line-through;';
			var fieldId = fieldIdFrom( $value );
			var calField = $wrap.attr( 'data-cal-field' ) || '';
			var srcSel   = calField ? '[name="input_' + calField + '"],[name^="input_' + calField + '."]' : '';
			var $msg    = $( '<p class="drezsync-booking__msg" style="margin:6px 0 0;font-size:14px;"></p>' ).appendTo( $wrap );
			var slotCal = null;

			function sourceVal() {
				if ( ! calField ) {
					return '';
				}
				var $els = $( srcSel );
				var $chk = $els.filter( ':checked' );
				if ( $chk.length ) {
					return $chk.first().val();
				}
				var v = $els.val();
				return ( null == v ) ? '' : v;
			}

			$date.attr( 'min', min );

			function hint( text ) {
				$times.html( '<p class="drezsync-booking__hint" style="margin:0;font-size:14px;opacity:.65;"></p>' );
				$times.find( '.drezsync-booking__hint' ).text( text );
			}

			function setMsg( text, col ) {
				if ( ! text ) {
					$msg.text( '' ).hide();
					return;
				}
				$msg.text( text ).css( 'color', col || '' ).show();
			}

			function chosen() {
				var v = $value.val() || '';
				var m = v.match( /^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2})$/ );
				return m ? { date: m[1], time: m[2] } : null;
			}


			// Dim the times that clash with another field, warn if our own pick clashes.
			function refresh() {
				var date = $date.val();
				var pick = chosen();

				$times.find( '.drezsync-booking__time-pill' ).each( function() {
					var $b = $( this );
					var t  = $b.attr( 'data-time' );
					if ( ! t || ! date ) {
						return;
					}
					var isPick  = pick && pick.time === t;
					var clashes = false;

					if ( isPick ) {
						$b.attr( 'style', clashes ? pillErr : pillOn ).attr( 'aria-pressed', 'true' ).removeAttr( 'data-clash' ).removeAttr( 'aria-disabled' );
					} else if ( clashes ) {
						$b.attr( 'style', pillDim ).attr( 'data-clash', '1' ).attr( 'aria-disabled', 'true' ).attr( 'aria-pressed', 'false' );
					} else {
						$b.attr( 'style', pillBase ).attr( 'aria-pressed', 'false' ).removeAttr( 'data-clash' ).removeAttr( 'aria-disabled' );
					}
				} );

				setMsg( '' );
			}

			function render( times ) {
				$times.empty();
				$.each( times, function( i, t ) {
					$( '<button type="button" class="drezsync-booking__time-pill"></button>' )
						.attr( 'data-time', t )
						.attr( 'aria-pressed', 'false' )
						.attr( 'style', pillBase )
						.text( pretty( t ) )
						.appendTo( $times );
				} );
				recompute();
			}

			// Load open times for the current date. Optionally keep a prior pick
			// selected if it is still available; otherwise clear it.
			function load( keepTime ) {
				var date = $date.val();
				if ( ! date ) {
					hint( drezsyncBooking.pickDate );
					recompute();
					return;
				}
				var data = {
					action:  'drezsync_availability',
					nonce:   drezsyncBooking.nonce,
					formId:  drezsyncBooking.formId,
					fieldId: fieldId,
					date:    date
				};
				if ( calField ) {
					var sv = sourceVal();
					if ( ! sv ) {
						$value.val( '' );
						hint( drezsyncBooking.pickSource );
						recompute();
						return;
					}
					data.calValue = sv;
				}
				hint( drezsyncBooking.loading );
				$.post( drezsyncBooking.ajaxUrl, data ).done( function( res ) {
					if ( res && res.success && res.data.times && res.data.times.length ) {
						render( res.data.times );
						if ( keepTime ) {
							$value.val( res.data.times.indexOf( keepTime ) !== -1 ? ( date + ' ' + keepTime ) : '' );
							recompute();
						}
					} else {
						hint( drezsyncBooking.none );
						recompute();
					}
				} ).fail( function() {
					hint( drezsyncBooking.none );
					recompute();
				} );
			}

			$date.on( 'change', function() {
				$value.val( '' );
				load();
			} );

			$times.on( 'click', '.drezsync-booking__time-pill', function() {
				var $btn = $( this );
				if ( '1' === $btn.attr( 'data-clash' ) ) {
					return; // A clashing time cannot be picked.
				}
				var date = $date.val();
				var time = $btn.attr( 'data-time' );
				$value.val( ( date && time ) ? ( date + ' ' + time ) : '' );
				recompute();
			} );

			if ( calField ) {
				$( document ).on( 'change', srcSel, function() {
					$value.val( '' );
					if ( $date.val() ) {
						load();
					} else {
						hint( drezsyncBooking.pickDate );
					}
					if ( slotCal ) {
						slotCal.reset();
						slotCal.draw();
					}
					recompute();
				} );
			}

			var reg = { refresh: refresh };

			var $cal = $wrap.find( '.drezsync-booking__calendar' );
			if ( $cal.length ) {
				slotCal = makeCalendar( $cal, {
					mode:        'slot',
					fieldId:     fieldId,
					sourceValFn: calField ? sourceVal : null,
					color:       color,
					selectable:  function( ymd, s ) {
						return ymd >= min && !! s.open[ ymd ];
					},
					onPick:      function( ymd ) {
						$date.val( ymd ).trigger( 'change' );
						slotCal.repaint();
					},
					selectedYmd: function() {
						return $date.val() || null;
					}
				} );
				slotCal.draw();
			}

			registry.push( reg );

			// On first load, if a date is already set (a saved value or a
			// re-render after a failed submit), reload the full list of times
			// and keep the prior pick, so nothing has to be re-entered.
			$times.on( 'focusin', '.drezsync-booking__time-pill', function() { $( this ).css( { 'outline': '3px solid ' + color, 'outline-offset': '2px' } ); } ).on( 'focusout', '.drezsync-booking__time-pill', function() { $( this ).css( { 'outline': '', 'outline-offset': '' } ); } );

				var start = chosen();
			if ( $date.val() ) {
				load( start ? start.time : null );
			}
		}


		$( '.drezsync-booking' ).each( function() {
			var $wrap = $( this );
			initSlot( $wrap );
		} );
	} );
}( jQuery ) );
